Self-hosted infrastructure stack: SSO/OIDC, Active Directory, reverse proxy, DNS β optimized for minimal resource usage.
This is a curated, secrets-free extract of my production Docker Compose stack. The full setup runs on a Debian 13 homelab server managed with OpenMediaVault, with additional services and configuration not included here. Only compose files are shared β
.envfiles are excluded as they contain secrets/passwords.
Why Docker Compose instead of Kubernetes? For a single-node setup, Kubernetes adds orchestration overhead without real benefit. Docker Compose gives you the same containerized services with a fraction of the resource usage β the entire stack (25+ containers) runs at ~28W idle and ~3GB RAM. I run a separate Talos Kubernetes cluster for learning and testing β but for production use on a single host, Compose is the pragmatic choice.
Why Authelia instead of Keycloak? Keycloak is powerful but heavy β it needs its own database, eats 500MB+ RAM at idle, and is built for enterprise-scale identity federation. Authelia is lightweight, config-file-driven, and does exactly what's needed: SSO, OIDC, 2FA β without the resource overhead. Paired with SWAG as reverse proxy, it adds authentication to any service with a single nginx directive.
Why Samba-DC instead of FreeIPA or LDAP-only? Samba-DC is one of the few realistic ways to run an AD-compatible domain controller inside Docker β with DNS, Kerberos, LDAP, and Group Policy support. The main reason: centralized SSO for both Windows and Mac clients, with domain join, AD-native group management, and Kerberos authentication out of the box. FreeIPA didn't work well in Docker when I tested it. Plain OpenLDAP would handle basic authentication but doesn't give you the AD functionality needed for Windows domain join and Group Policy.
Why Uptime Kuma instead of Prometheus/Grafana?
A full-blown Prometheus stack is not necessary for a single-node host. Uptime Kuma handles endpoint monitoring with email alerts when something goes down β that's all that's needed. Email alerting is configured through OpenMediaVault at the host level. For system stats, OMV, Arcane, or just btop on the host do the job without running a time-series database, exporters, and Grafana dashboards.
graph TD
Internet(["π Internet"])
Internet --> SWAG
subgraph Core Infrastructure
SWAG["π SWAG<br/>Reverse Proxy & SSL"]
SWAG --> Authelia["π‘οΈ Authelia<br/>SSO / OIDC / 2FA"]
Authelia -.->|LDAP auth| SambaDC["π’ Samba-DC<br/>Active Directory"]
end
subgraph Application Services
Authelia --> Nextcloud["βοΈ Nextcloud<br/>Cloud Storage"]
Authelia --> Arcane["π Arcane<br/>Dashboard"]
end
subgraph DNS & Network
Pihole["π§Ή Pi-hole + Unbound<br/>Local DNS"]
Dockerproxy["π Docker Proxy<br/>Secured Socket"]
end
| Folder | Service | Description |
|---|---|---|
1-swag/ |
SWAG | Reverse proxy with automatic SSL (Let's Encrypt) |
authelia/ |
Authelia | Single Sign-On & OIDC provider |
samba-dc/ |
Samba DC | Active Directory domain controller |
pihole/ |
Pi-hole | DNS server with ad-blocking (+ Unbound) |
dockerproxy/ |
Docker Proxy | Secured access to the Docker socket |
arcane/ |
Arcane | Homelab dashboard / service overview |
nextcloud/ |
Nextcloud | Self-hosted cloud storage β initial setup via browser on first start |
Samba-DC provides an Active Directory backend. Authelia sits in front of services and handles:
- OIDC authentication for applications
- 2FA / MFA enforcement
- Single Sign-On across all proxied services
The file samba-dc/dns-records contains DNS entries for your domain (e.g. domain.org).
# Import DNS records into Samba-DC
./samba-dc/import-dns-records.sh
# Register a new OIDC client with Authelia
./authelia/add-client.shAuthelia loads secrets from files in authelia/secrets/ (gitignored β create them on each host).
| Secret file | Purpose | Min. length |
|---|---|---|
AUTHELIA_PASSWORD |
Password for the Authelia LDAP service account | 16 chars |
EMAIL_PASSWORD |
Password for the notification mail account | 16 chars |
ENCRYPTION_SECRET |
Storage encryption key | 20 chars (Authelia enforced) |
JWT_SECRET |
JWT signing secret (used for password reset) | 20 chars |
SESSION_SECRET |
Session cookie signing secret | 20 chars |
oidc/HMAC_SECRET |
HMAC secret for OIDC token signing | 32 chars |
Generate all secrets at once:
mkdir -p authelia/secrets/oidc
openssl rand -base64 32 > authelia/secrets/AUTHELIA_PASSWORD
openssl rand -base64 32 > authelia/secrets/EMAIL_PASSWORD
openssl rand -base64 48 > authelia/secrets/ENCRYPTION_SECRET
openssl rand -base64 48 > authelia/secrets/JWT_SECRET
openssl rand -base64 48 > authelia/secrets/SESSION_SECRET
openssl rand -base64 48 > authelia/secrets/oidc/HMAC_SECRETNote:
authelia/config/configuration.ymlis an example configuration. At minimum, adjust alldomain.orgreferences and the LDAPuserDN to match your setup.
See authelia/authelia.yml for the expected secret paths.
Samba-DC runs on a macvlan interface with a dedicated LAN IP so it can act as a proper AD domain controller (ports 53, 389, 445, 636).
The domain admin password is passed as a Docker secret β create the file before starting:
# Generate a random admin password
openssl rand -base64 32 > samba-dc/samba_admin_pass
samba_admin_passis gitignored and must be created manually on each host.
SWAG handles all inbound traffic, SSL termination, and forwards requests to backend services.
Custom proxy configs live in 1-swag/custom-proxies/.
Create 1-swag/cloudflare.ini (gitignored) with your Cloudflare credentials:
# Option A β API Token (recommended, scoped to Zone:DNS:Edit)
dns_cloudflare_api_token = YOUR_CLOUDFLARE_API_TOKEN
# Option B β Global API Key (legacy)
# dns_cloudflare_email = your@email.com
# dns_cloudflare_api_key = YOUR_GLOBAL_API_KEYPi-hole acts as the local DNS server with ad-blocking.
Unbound runs as a sidecar container (madnuttah/unbound) and is configured via the image's built-in defaults.
Several services rely on the following variables being present in the shell environment:
| Variable | Description | Example |
|---|---|---|
TZ |
Timezone | Europe/Berlin |
PUID |
User ID for file permissions | 1000 |
PGID |
Group ID for file permissions | 1000 |
On this host these are injected globally by OpenMediaVault. If you run the stack outside OMV, set them in your shell or add them to each service's .env file:
export TZ=Europe/Berlin
export PUID=1000
export PGID=1000All services use external Docker networks that must exist on the host before starting:
docker network create swag
docker network create dockerproxy
docker network create pihole
docker network create -d macvlan \
--subnet=192.168.178.0/24 \
--gateway=192.168.178.1 \
-o parent=eth0 \
macvlan0
macvlan0is only required by Samba-DC (needs a dedicated IP on the LAN). Adjust subnet, gateway, and parent interface to match your network.
Images are pulled from dhi.io instead of Docker Hub for faster and more complete CVE fixes. Some images here use it (uptime-kuma, postgres, redis).
If you'd rather use the official images, drop-in replacements:
dhi.io image |
Official replacement |
|---|---|
dhi.io/uptime-kuma:2 |
louislam/uptime-kuma:2 |
dhi.io/postgres:18-alpine3.22 |
postgres:18-alpine |
dhi.io/redis:8 |
redis:8-alpine |
Handled via BorgBackup at the host level (managed through OpenMediaVault, not part of this Compose stack).
-
Clone the repo
-
Copy each
*.envfile (not included) and fill in your values -
For Authelia: define required secrets in the
authelia/folder -
Start the stack β recommended order:
samba-dc β pihole β authelia β swag β remaining services