A modular homeserver setup using Docker Compose with Traefik reverse proxy, Authelia authentication, Pi-hole ad blocking, WireGuard VPN, n8n automation, and more.
homeserver/
├── .env # All environment variables (common + service-specific)
├── traefik/
│ └── docker-compose.yml # Traefik reverse proxy
├── authelia/
│ ├── docker-compose.yml # Authentication service
│ └── config/
├── <service>/
│ └── docker-compose.yml # Each service has its own docker-compose.yml
└── ... # More services can be added
https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository
Repo -> Settings -> Actions -> Runners -> New self-hosted runner Then follow the simple instructions
Install the runner service:
sudo ./svc.sh installEnable and start the service:
systemctl enable actions.runner.<TAB><TAB>
systemctl start actions.runner.<TAB><TAB>Create a .env file in the root directory:
cp .env.example .envEdit .env with ALL your values (common + service-specific):
# Common variables
DATADIR=/path/to/data/directory
CONFIGDIR=/path/to/config/directory
DOMAIN_NAME=example.com
LOCAL_IP=server.local.ip.address
SSL_EMAIL=your-email@example.com
TIMEZONE=Asia/Jerusalem
# User/Group IDs (get with: id -u and id -g)
PUID=1000
PGID=1000
# Cloudflare API token (for Traefik SSL certificates and DDNS)
CF_DNS_API_TOKEN=<your-cloudflare-api-token>
CF_DNS_ZONE_ID=<your-cloudflare-zone-id>
# Authelia secrets (generate using command below)
JWT_SECRET=<generated-secret>
SESSION_SECRET=<generated-secret>
STORAGE_ENCRYPTION_KEY=<generated-secret>
# SMTP configuration (for email notifications)
SMTP_USERNAME=your-email@gmail.com
SMTP_PASSWORD=<gmail-app-password>
# Servarr API Keys (found in Settings → General → Security in each app)
SONARR_API_KEY=<your-sonarr-api-key>
RADARR_API_KEY=<your-radarr-api-key>
PROWLARR_API_KEY=<your-prowlarr-api-key>
BAZARR_API_KEY=<your-bazarr-api-key>
# Media Server API Keys
JELLYFIN_API_KEY=<your-jellyfin-api-key>
JELLYSEER_API_KEY=<your-jellyseerr-api-key>
# qBittorrent credentials
QBIT_USER=<your-qbittorrent-username>
QBIT_PASS=<your-qbittorrent-password>
# WireGuard (WG-Easy) credentials for Homepage widget
WIREGUARD_USERNAME=<your-wireguard-username>
WIREGUARD_PASSWORD=<your-wireguard-password>
# Pi-hole API Key (found in Settings → API / Web interface)
PIHOLE_API_KEY=<your-pihole-api-key>
# CrowdSec (generate bouncer key after first deployment)
CROWDSEC_BOUNCER_KEY=<generate-after-deployment>
CROWDSEC_ENROLL_KEY=<optional-for-console>
# JOAL (Jack of All Trades torrent ratio faker)
JOAL_SECRET_TOKEN=<random-secret-string>
JOAL_SECRET_OBFUSCATION_PATH=<random-path-string>
# Restic Backup Configuration
RESTIC_PASSWORD=<generate-with-openssl-rand-base64-32>
# Paperless-ngx Configuration
PAPERLESS_SECRET_KEY=<generate-with-openssl-rand-base64-32>
# SparkyFitness Configuration
SPARKY_FITNESS_DB_NAME=sparkyfitness_db
SPARKY_FITNESS_DB_USER=sparky
SPARKY_FITNESS_DB_PASSWORD=<generate-with-openssl-rand-base64-32>
SPARKY_FITNESS_APP_DB_USER=<app-db-username>
SPARKY_FITNESS_APP_DB_PASSWORD=<generate-with-openssl-rand-base64-32>
SPARKY_FITNESS_API_ENCRYPTION_KEY=<generate-with-openssl-rand-base64-32>
SPARKY_FITNESS_JWT_SECRET=<generate-with-openssl-rand-base64-32>docker run --rm authelia/authelia:latest authelia crypto rand --length 64Run this command three times to generate the three secrets, then add them to .env.
To use Cloudflare for SSL certificates (via DNS challenge) and dynamic DNS updates:
- Go to Cloudflare API Tokens
- Click "Create Token"
- Use the "Edit zone DNS" template
- Configure permissions:
- Permissions:
Zone - DNS - Edit - Zone Resources:
Include - Specific zone - <your-domain>
- Permissions:
- Click "Continue to summary" and "Create Token"
- Add this token to your
.envfile asCF_DNS_API_TOKEN
To use Gmail for email notifications, you need to generate an app password:
- Go to Google App Passwords
- Enable 2-factor authentication if you haven't already
- Select "Mail" and your device
- Click "Generate" to get a 16-character password
- Add this password to your
.envfile asSMTP_PASSWORD
Set these as GitHub Actions variables/secrets (Settings → Secrets and variables → Actions):
Variables (public):
| Variable | Service | Notes |
|---|---|---|
DATADIR |
All | Path to data directory on server |
CONFIGDIR |
All | Path to config directory on server |
TIMEZONE |
All | Timezone (e.g., Asia/Jerusalem) |
Secrets (encrypted):
| Secret | Service | Notes |
|---|---|---|
DOMAIN_NAME |
All | Your domain name |
LOCAL_IP |
All | Your local server IP |
PUID |
All | User ID (typically 1000) |
PGID |
All | Group ID (typically 1000) |
SSL_EMAIL |
Traefik | Email for Let's Encrypt notifications |
CF_DNS_API_TOKEN |
Traefik, Cloudflare-DDNS | Cloudflare API token with DNS edit permissions |
CF_DNS_ZONE_ID |
Cloudflare-DDNS | Cloudflare zone ID (found in domain dashboard) |
JWT_SECRET |
Authelia | Generate with: docker run --rm authelia/authelia:latest authelia crypto rand --length 64 |
SESSION_SECRET |
Authelia | Generate with: docker run --rm authelia/authelia:latest authelia crypto rand --length 64 |
STORAGE_ENCRYPTION_KEY |
Authelia | Generate with: docker run --rm authelia/authelia:latest authelia crypto rand --length 64 |
SMTP_USERNAME |
Authelia | Your Gmail address (e.g., your-email@gmail.com) |
SMTP_PASSWORD |
Authelia | Gmail app password (generate at https://myaccount.google.com/apppasswords) |
SONARR_API_KEY |
Recyclarr, Unpackerr, Homepage | Sonarr API key (found in Sonarr → Settings → General → Security) |
RADARR_API_KEY |
Recyclarr, Unpackerr, Homepage | Radarr API key (found in Radarr → Settings → General → Security) |
PROWLARR_API_KEY |
Homepage | Prowlarr API key (found in Prowlarr → Settings → General → Security) |
BAZARR_API_KEY |
Homepage | Bazarr API key (found in Bazarr → Settings → General → Security) |
JELLYFIN_API_KEY |
Homepage | Jellyfin API key (create in Dashboard → API Keys) |
JELLYSEER_API_KEY |
Homepage | Jellyseerr API key (found in Settings → General) |
QBIT_USER |
Homepage | qBittorrent username |
QBIT_PASS |
Homepage | qBittorrent password |
WIREGUARD_USERNAME |
Homepage | WireGuard (WG-Easy) username |
WIREGUARD_PASSWORD |
Homepage | WireGuard (WG-Easy) password |
PIHOLE_API_KEY |
Homepage | Pi-hole API key (found in Settings → API / Web interface) |
CROWDSEC_BOUNCER_KEY |
Traefik, CrowdSec | Generate with: docker exec crowdsec cscli bouncers add traefik-bouncer (after first CrowdSec deployment) |
CROWDSEC_ENROLL_KEY |
CrowdSec | Optional - For CrowdSec Console enrollment (get from https://app.crowdsec.net/) |
JOAL_SECRET_TOKEN |
JOAL | Random secret string for UI authentication |
JOAL_SECRET_OBFUSCATION_PATH |
JOAL | Random path string to obfuscate UI URL (e.g., my-secret-path-123) |
PAPERLESS_SECRET_KEY |
Paperless-ngx | Generate with: openssl rand -base64 32 |
SPARKY_FITNESS_DB_NAME |
SparkyFitness | PostgreSQL database name (e.g., sparkyfitness_db) |
SPARKY_FITNESS_DB_USER |
SparkyFitness | PostgreSQL database user (e.g., sparky) |
SPARKY_FITNESS_DB_PASSWORD |
SparkyFitness | PostgreSQL database password - Generate with: openssl rand -base64 32 |
SPARKY_FITNESS_APP_DB_USER |
SparkyFitness | Application database username |
SPARKY_FITNESS_APP_DB_PASSWORD |
SparkyFitness | Application database password - Generate with: openssl rand -base64 32 |
SPARKY_FITNESS_API_ENCRYPTION_KEY |
SparkyFitness | API encryption key - Generate with: openssl rand -base64 32 |
SPARKY_FITNESS_JWT_SECRET |
SparkyFitness | JWT authentication secret - Generate with: openssl rand -base64 32 |
Copy the example file and edit it:
cp authelia/config/users.yaml.example authelia/config/users.yamlConfigure hashed password:
docker run --rm -it authelia/authelia:4.39.11 authelia crypto hash generate argon2To configure Cloudflare Dynamic DNS:
- Copy the example configuration file to your config directory:
cp cloudflare-ddns/config.json.example ${CONFIGDIR}/cloudflare-ddns/config.json- Edit the configuration file and update the required fields:
nano ${CONFIGDIR}/cloudflare-ddns/config.jsonKey fields to configure:
zone_id: Your Cloudflare zone ID (found in your domain's dashboard)subdomains: Configure which subdomains to update (@for root domain)proxied: Set totrueto enable Cloudflare proxy, orfalsefor direct DNS
Note: The ${CF_DDNS_API_TOKEN} will be automatically substituted from your environment variables.
CrowdSec protects your homeserver by analyzing Traefik access logs and blocking malicious IPs using the Traefik bouncer plugin.
- Deploy CrowdSec (along with Traefik):
export $(cat .env | xargs)
docker compose -f traefik/docker-compose.yml up -d
docker compose -f crowdsec/docker-compose.yml up -d- Generate a bouncer API key:
docker exec crowdsec cscli bouncers add traefik-bouncerThis command will output an API key. Copy it and add it to your .env file as CROWDSEC_BOUNCER_KEY.
- Restart Traefik to apply the bouncer key:
docker compose -f traefik/docker-compose.yml restart- (Optional) Enroll in the CrowdSec Console for centralized management:
docker exec crowdsec cscli console enroll <your-enroll-key>Get your enrollment key from: https://app.crowdsec.net/
A self-hosted Metabase dashboard is available at https://crowdsec.${DOMAIN_NAME} for visualizing CrowdSec metrics, alerts, and decisions.
Initial Setup (First Time Only):
-
Access the dashboard at
https://crowdsec.${DOMAIN_NAME} -
Complete the Metabase setup wizard:
- Set your preferred language
- Create an admin account
- Skip "Add your data" (we'll add it manually)
-
Add CrowdSec database connection:
- Click "Settings" (gear icon) → "Admin settings" → "Databases" → "Add database"
- Database type: SQLite
- Display name: CrowdSec
- Filename:
/crowdsec-db/crowdsec.db - Click "Save"
For services with Authelia:
- traefik.http.routers.<service>.middlewares=crowdsec-bouncer@docker,authelia-forwardauth@dockerFor services without Authelia:
- traefik.http.routers.<service>.middlewares=crowdsec-bouncer@dockerTo add CrowdSec to new services in the future, simply add the appropriate middleware label to the service's Traefik configuration.
-
Set the two required secrets in your
.envfile:JOAL_SECRET_TOKEN: A random secret string for authentication (e.g., useopenssl rand -hex 32)JOAL_SECRET_OBFUSCATION_PATH: A random path to hide the UI URL (e.g.,my-secret-path-123)
-
Access the web UI at:
http://${LOCAL_IP}:8584/${JOAL_SECRET_OBFUSCATION_PATH}/ui
Automated backup solution using Restic with Google Drive (via rclone) as the backend storage.
- Automated Backups: Daily backups at 3:00 AM
- Intelligent Storage: Deduplication and compression to save space
- Retention Policy: Keeps 7 daily, 4 weekly, 4 monthly
- Encryption: All data encrypted with AES-256 before upload
- Google Drive Backend: Cloud storage via rclone
- Generate a strong password for encryption:
openssl rand -base64 32Add this to your .env file as RESTIC_PASSWORD.
-
Configure Google Drive authentication with rclone (see
restic/README.mdfor detailed instructions) -
Configure which directories to backup in
restic/config/backup-paths.txt -
Configure backup path in gdrive in
RESTIC_REPOSITORYenvironment variable. -
Run initial backup:
docker exec restic /scripts/backup.shFor detailed documentation, usage examples, and troubleshooting, see restic/README.md.
Ensure the following ports are open in your firewall:
| Port | Protocol | Service | Purpose |
|---|---|---|---|
| 80 | TCP | Traefik | HTTP (auto-redirects to HTTPS) |
| 443 | TCP | Traefik | HTTPS |
| 51820 | UDP | WG-Easy | WireGuard VPN |
Important: Always export environment variables first before running any docker compose commands:
export $(cat .env | xargs)Deploy services in the following order:
1. Deploy Traefik first (creates required networks):
docker compose -f traefik/docker-compose.yml up -d2. Deploy all other services automatically:
for dir in */; do [ -f "$dir/docker-compose.yml" ] && [ "$dir" != "traefik/" ] && docker compose -f "$dir/docker-compose.yml" up -d; doneOr deploy individual services manually:
docker compose -f authelia/docker-compose.yml up -dNote: The export $(cat .env | xargs) command loads environment variables from the root .env file into your shell, making them available to docker-compose for variable substitution. Remember to run this command in each new shell session.
After deployment, you can access your services at:
- Authelia:
https://auth.${DOMAIN_NAME} - Pi-hole:
https://pihole.${DOMAIN_NAME}(requires Authelia login) - WG-Easy:
https://wg-easy.${DOMAIN_NAME}(requires Authelia login) - n8n:
https://n8n.${DOMAIN_NAME}(requires Authelia login)
On first access to protected services, you'll be redirected to Authelia for authentication using the credentials configured in authelia/config/users.yaml.
This project uses Renovate for automated dependency updates.
- Schedule: Sundays between 3:00 AM - 7:00 AM (Asia/Jerusalem timezone)
- Auto-merge: Minor and patch version updates are automatically merged
- Manual review: Major version updates require manual approval
- Discovery: Automatically finds all
docker-compose.ymlfiles in the repository - Security: Docker images are pinned with digests for immutable references
Renovate configuration is in .github/renovate.json
-
Install restic and rclone.
-
Authenticate rclone with
rclone.conffile
docker run --rm -it -v $(pwd):/config/rclone rclone/rclone:latest config reconnect "gdrive:" --config /config/rclone/rclone.confChoose "No" and run the rclone command from output in other terminal window to get the token, then insert it.
- Set those 3 environment variables
export RESTIC_REPOSITORY=rclone:gdrive:<RESTIC_REPO_PATH_INSIDE_GDRIVE>
export RCLONE_CONFIG=<PATH_TO_rclone.conf>
export RESTIC_PASSWORD=<RESTIC_PASSWORD>-
List snapshots
restic snapshots -
restic restore <SNAPSHOT_ID> --target <TARGET_DIR>