Self-hosted Docker stack on Proxmox · Managed via Forgejo CI/CD · Automatic TLS · 2FA on everything
Internet
│
▼
Cloudflare DNS (DNS-only, no proxy)
│
▼
Traefik v3 (reverse proxy · TLS termination)
├── CrowdSec bouncer (IP blocking)
└── Authelia (2FA / SSO)
│
├── 🌐 Public services (Authelia-protected)
│ ├── Nextcloud · Immich · Vaultwarden
│ ├── n8n · Grafana · Guacamole · Portainer
│ └── ntfy · Forgejo · Excalidraw
│
└── 🔒 VPN-only
├── Homepage dashboard
├── Proxmox UI
└── Router UI
WireGuard ── 10.x.x.0/24
Tailscale ── overlay network
Service
Description
Auth
Traefik
Reverse proxy, automatic TLS via Cloudflare DNS challenge
—
Authelia
2FA / SSO provider for all exposed services
—
CrowdSec
Intrusion detection + Traefik bouncer for IP blocking
—
WireGuard
VPN server
—
ddclient
Dynamic DNS updater
—
Service
Description
Auth
Nextcloud
Primary cloud storage & collaboration
Nextcloud
Nextcloud Media
Secondary Nextcloud instance for media
Nextcloud
Immich
Self-hosted photo & video library with ML
Immich
Vaultwarden
Bitwarden-compatible password manager
Vaultwarden
Service
Description
Auth
n8n
Low-code workflow automation
Authelia
Forgejo
Self-hosted Git forge
Forgejo
Forgejo Runner
CI/CD pipeline executor
—
Renovate
Automated dependency updates
—
Service
Description
Auth
Grafana
Dashboards & visualization
Authelia
Prometheus
Metrics collection & storage
—
Alertmanager
Alert routing → ntfy
—
cAdvisor
Container metrics
—
Node Exporter
Host system metrics
—
ntfy
Push notification broker
ntfy auth
Service
Description
Auth
Portainer
Docker management UI
Authelia
Guacamole
Clientless remote desktop gateway
Authelia
Homepage
Service dashboard
Local/VPN only
Excalidraw
Self-hosted whiteboard
Authelia
Watchtower
Container image auto-updater
—
Powered by Forgejo Actions with three pipelines:
Workflow
Trigger
What it does
deploy.yml
Push to master
Detects changed services, SSH-deploys only those
renovate.yml
Weekly (Sat 8AM)
Opens PRs to update pinned Docker image versions
security-scan.yml
Push + weekly (Sun 4AM)
Runs Gitleaks (secrets) + Grype (CVEs)
Auto-merge is enabled for minor/patch updates. Major updates and security-critical services (traefik, authelia, crowdsec) require manual review.
Docker + Docker Compose v2
A domain with Cloudflare DNS
Cloudflare API token with DNS edit permissions
Create the shared Traefik network
docker network create traefik
cd < service-directory>
cp .env.example .env
# Fill in .env with real values
docker compose up -d
1. networking/traefik
2. networking/crowdsec
3. networking/authelia
4. Everything else (any order)
Alertmanager doesn't support env var substitution natively. Generate the config before starting:
export $( grep -v ' ^#' monitoring/.env | xargs)
envsubst < monitoring/alertmanager/alertmanager.yml.tmpl > monitoring/alertmanager/alertmanager.yml
Mount
Purpose
/
Root filesystem, Docker volumes
/mnt/immich
Immich original photos
/mnt/immich-thumbs
Immich thumbnails & previews
/mnt/media
Shared media files
/mnt/small-nxtc
Nextcloud Media instance data
/mnt/forgejo
Forgejo repositories
Every service follows the same conventions:
Independent compose files — each service has its own compose.yml, deployed separately
Pinned image versions — all images locked to specific tags with # renovate: datasource=docker for auto-updates
.env for secrets — never committed; .env.example provided as template
Security hardening — no-new-privileges:true, cap_drop: ALL, minimal cap_add
Resource limits — CPU and memory limits on every container
Healthchecks — 30s interval, 5s timeout, 3 retries on all services
Traefik labels — standardized label pattern for routing, TLS, and middleware
Variable
Description
PUID / PGID
User/group ID (typically 1000)
TZ
Timezone (e.g. Europe/Madrid)
TRAEFIK_DOMAIN
Service subdomain
TRAEFIK_ROUTER_NAME
Unique router name for Traefik
TRAEFIK_CERT_RESOLVER
Certificate resolver (cloudflare)