added nginx to traefik migration changes and docs
This commit is contained in:
27
README.md
27
README.md
@@ -34,7 +34,8 @@ This is a real, running homelab — not a demo. It's deployed on a home Ubuntu s
|
||||
| **Sentinel** | `docker-compose/sentinel/` | Disk health monitoring integrated with smartctl, dumps output to container logs |
|
||||
| **smartctl** | `docker-compose/smartctl/` | Disk health monitoring (S.M.A.R.T. data) |
|
||||
| **OpenHands** | `docker-compose/openhands/` | Self-hosted AI coding agent (OpenHands/agent-canvas) backed by Ollama — see [AI Coding Agent](docs/AI-Agent.md) |
|
||||
| **Nginx Proxy Manager** | `docker-compose/nginx-proxy-manager/` | Reverse proxy giving every service a friendly `.home` hostname with locally-trusted HTTPS — see [Reverse Proxy](docs/Reverse-Proxy.md) |
|
||||
| **Traefik** | `docker-compose/traefik/` | Reverse proxy with Docker daemon auto-discovery and full config-as-code — replaces Nginx Proxy Manager — see [Traefik](docs/Traefik.md) |
|
||||
| **Vaultwarden** | `docker-compose/vaultwarden/` | Self-hosted Bitwarden-compatible password manager — see [Vaultwarden](docs/Vaultwarden.md) |
|
||||
| **Semaphore** | `docker-compose/semaphore/` | Self-hosted Ansible UI (alternative to AWX) — see [Semaphore Guide](docs/Semaphore.md) |
|
||||
| **Ansible playbooks** | `ansible/update-playbooks/` | Shared update role (pull → recreate → health-check → prune) reused across every stack, run via Semaphore — see [Ansible Playbooks](docs/AnsiblePlaybooks.md) |
|
||||
| **Server provisioning** | `ansible/preparing-playbook/` | Full server preparation playbook (packages, hardening, Docker, Nexus/Traefik) tested against Vagrant first — see [Ansible README](ansible/README.md) |
|
||||
@@ -50,6 +51,7 @@ Not everything in this repo runs permanently. Some folders are kept purely as re
|
||||
|
||||
| Lab | Folder | Purpose |
|
||||
|---|---|---|
|
||||
| **Nginx Proxy Manager** | `docker-compose/nginx-proxy-manager/` | ~~Retired~~ — replaced by Traefik for config-as-code reverse proxying. See [Traefik](docs/Traefik.md) for the migration rationale and [Reverse Proxy](docs/Reverse-Proxy.md) for the original NPM setup |
|
||||
| **Elastic Stack (ELK)** | `docker-compose/ELK/` | Elasticsearch + Kibana, security-enabled, sized for this hardware — a modern-workflow (Elastic Agent/Fleet) alternative to an older Filebeat/Metricbeat-based course reference. Not run continuously since Loki now covers centralized logging here — see [Elastic Stack (ELK)](docs/ElasticStack.md) |
|
||||
| **Vagrant test VM** | `vagrant/` | Disposable Debian 12 VirtualBox VM (`bento/debian-12`) on a private-network IP, for testing provisioning scripts and configuration changes before touching the real host |
|
||||
|
||||
@@ -100,8 +102,24 @@ homelab/
|
||||
│ │ └── docker-compose.yml
|
||||
│ ├── openhands/
|
||||
│ │ └── docker-compose.yaml
|
||||
│ ├── traefik/
|
||||
│ │ ├── compose.yaml
|
||||
│ │ ├── README.md
|
||||
│ │ ├── certs/
|
||||
│ │ │ ├── home.server.crt
|
||||
│ │ │ └── home.server.key
|
||||
│ │ └── dynamic/
|
||||
│ │ ├── tls.yaml
|
||||
│ │ ├── middlewares.yaml
|
||||
│ │ ├── pihole.yaml
|
||||
│ │ ├── plex.yml
|
||||
│ │ ├── sentinel.yml
|
||||
│ │ └── dockscope-auth.yml
|
||||
│ ├── vaultwarden/
|
||||
│ │ ├── docker-compose.yml
|
||||
│ │ └── vw-data/
|
||||
│ ├── nginx-proxy-manager/
|
||||
│ │ └── docker-compose.yaml
|
||||
│ │ └── compose.yaml
|
||||
│ ├── semaphore/
|
||||
│ │ ├── docker-compose.yml
|
||||
│ │ └── README.md
|
||||
@@ -216,9 +234,9 @@ Rather than pulling and recreating manually, every always-on stack has a matchin
|
||||
## Security notes
|
||||
|
||||
- All `.env` files, runtime config directories, and databases are excluded via `.gitignore` — only deployment definitions (`docker-compose.yml`) and `.env.example` placeholders are tracked.
|
||||
- Passwords referenced in compose files are injected via environment variables, never hardcoded.
|
||||
- Passwords referenced in compose files are injected via environment variables, never hardcoded. Long passwords (30+ chars) are generated and stored in Vaultwarden, referenced via `.env` files on the host.
|
||||
- The AI coding agent (`docker-compose/openhands/`) currently uses a third-party API aggregator as its LLM backend — see [AI Coding Agent](docs/AI-Agent.md) for details and caveats. It has no access to the Docker socket or other containers on the host.
|
||||
- **DockScope** (`docker-compose/dockscope/`) mounts `/var/run/docker.sock`, the same as Portainer — this grants it effectively root-equivalent access to the Docker host, and it currently has no built-in authentication on its web UI, so it's kept LAN-only and not exposed through the reverse proxy. See [DockScope](docs/DockScope.md).
|
||||
- **DockScope** (`docker-compose/dockscope/`) mounts `/var/run/docker.sock`, the same as Portainer — this grants it effectively root-equivalent access to the Docker host. It's protected by a Traefik basicAuth middleware and kept LAN-only. See [DockScope](docs/DockScope.md).
|
||||
- **Grafana Alloy** (part of the monitoring stack) also mounts `/var/run/docker.sock` read-only, needed to discover containers and tail their logs.
|
||||
|
||||
## Lessons learned
|
||||
@@ -244,6 +262,7 @@ Rather than pulling and recreating manually, every always-on stack has a matchin
|
||||
- `kube-state-metrics`'s default ClusterRole doesn't cover every resource it tries to watch (`NetworkPolicy`, `MutatingWebhookConfiguration`, `ValidatingWebhookConfiguration`, and more) — surfaces as repeated `reflector.go` `Failed to list ... is forbidden` warnings rather than a single clear error. Fixed with a supplementary `ClusterRole`/`ClusterRoleBinding` granting just the missing `list`/`watch` verbs. See [K3s](docs/K3s.md).
|
||||
- A pod doesn't pick up a widened RBAC grant on its own — its informers already failed and cached that state. `kubectl rollout restart` is required after any `ClusterRole` change for an already-running pod to actually benefit from it.
|
||||
- Old log lines don't disappear from Grafana/Loki after a fix ships — they just age out of whatever time window the dashboard is querying. A dropping "Errors total" over a few minutes is the real signal that a fix worked, not an empty panel immediately after applying it.
|
||||
- Migrating from a UI-based reverse proxy (Nginx Proxy Manager) to a config-as-code one (Traefik) forced a cleanup of every hardcoded password in every compose file — the migration was the catalyst for adopting Vaultwarden and proper secret management across the stack. See [Traefik](docs/Traefik.md) and [Vaultwarden](docs/Vaultwarden.md).
|
||||
|
||||
## Roadmap
|
||||
|
||||
|
||||
@@ -1,9 +1,48 @@
|
||||
# ELK + Fleet — sized for: Phenom II X4, 8GB DDR3, 120GB SSD
|
||||
#
|
||||
# Scope right now = what the course has actually covered: Elasticsearch,
|
||||
# Logstash, Kibana, Fleet Server. Filebeat/auditbeat/metricbeat/packetbeat/
|
||||
# apm-server are deliberately NOT in here yet — add them as their own
|
||||
# service blocks (same pattern as elasticsearch/kibana below) when you
|
||||
# reach that lesson, so you're not paying RAM for services you can't use
|
||||
# yet.
|
||||
#
|
||||
# Services are grouped with Compose "profiles" so you can turn whole
|
||||
# lesson-chunks on/off without editing this file — just edit
|
||||
# COMPOSE_PROFILES in .env. Core (elasticsearch + kibana) has no profile,
|
||||
# so it's always included.
|
||||
#
|
||||
# Rough memory budget on this box (no swap):
|
||||
# elasticsearch ~1.5 GB
|
||||
# logstash ~0.9 GB
|
||||
# kibana ~0.9 GB
|
||||
# fleet-server ~0.4 GB
|
||||
# ---------------------------------
|
||||
# total ~3.7 GB (you have ~5.9 GB free right now)
|
||||
#
|
||||
# That leaves headroom for Docker/Traefik/host overhead and for the first
|
||||
# enrolled Elastic Agent once you get there (~0.25-0.3 GB more).
|
||||
#
|
||||
# REGISTRY: docker.elastic.co sits behind Amazon CloudFront, which
|
||||
# geo-blocks a handful of sanctioned countries (confirmed pattern for
|
||||
# Russia/Belarus, same CloudFront-based mechanism affects Iran) — that's
|
||||
# what "context deadline exceeded" against docker.elastic.co/v2/ almost
|
||||
# always means, not a real network fault. These images pull instead from
|
||||
# Docker Hub (elasticsearch, kibana, logstash under the official
|
||||
# library/ namespace, elastic-agent under elastic/) — Elastic pushes the
|
||||
# exact same images there and keeps them current (checked: 8.19.19
|
||||
# present on all four as of this writing). Different CDN, so it isn't
|
||||
# guaranteed to be reachable either — test with
|
||||
# `docker pull elasticsearch:${ELK_VERSION}` before assuming it works.
|
||||
|
||||
services:
|
||||
elasticsearch:
|
||||
image: elasticsearch:8.19.19
|
||||
image: elasticsearch:${ELK_VERSION}
|
||||
container_name: elasticsearch
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- node.name=elasticsearch
|
||||
- cluster.name=elk-lab
|
||||
- discovery.type=single-node
|
||||
- ELASTIC_PASSWORD=${ELASTIC_PASSWORD}
|
||||
- xpack.security.enabled=true
|
||||
@@ -13,19 +52,37 @@ services:
|
||||
- ES_JAVA_OPTS=-Xms1g -Xmx1g
|
||||
mem_limit: 1500m
|
||||
cpus: 1.5
|
||||
ulimits:
|
||||
memlock:
|
||||
soft: -1
|
||||
hard: -1
|
||||
volumes:
|
||||
- es-data:/usr/share/elasticsearch/data
|
||||
ports:
|
||||
- "9200:9200"
|
||||
restart: unless-stopped
|
||||
- "9200:9200" # keep for now, drop once you trust Traefik routing
|
||||
networks:
|
||||
- elk
|
||||
- web_net
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -s -u elastic:${ELASTIC_PASSWORD} http://localhost:9200 | grep -q cluster_name"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
- "traefik.http.routers.elasticsearch.rule=Host(`elasticsearch.home.server`)"
|
||||
- "traefik.http.routers.elasticsearch.entrypoints=websecure"
|
||||
- "traefik.http.routers.elasticsearch.tls=true"
|
||||
- "traefik.http.routers.elasticsearch.service=elasticsearch"
|
||||
- "traefik.http.routers.elasticsearch.middlewares=security-headers@file"
|
||||
- "traefik.http.routers.elasticsearch-http.rule=Host(`elasticsearch.home.server`)"
|
||||
- "traefik.http.routers.elasticsearch-http.entrypoints=web"
|
||||
- "traefik.http.routers.elasticsearch-http.middlewares=redirect-to-https@file"
|
||||
- "traefik.http.services.elasticsearch.loadbalancer.server.port=9200"
|
||||
|
||||
kibana-setup:
|
||||
image: elasticsearch:8.19.19
|
||||
image: elasticsearch:${ELK_VERSION}
|
||||
container_name: kibana-setup
|
||||
depends_on:
|
||||
elasticsearch:
|
||||
@@ -43,13 +100,16 @@ services:
|
||||
- "Content-Type: application/json"
|
||||
- "-d"
|
||||
- "{\"password\":\"${KIBANA_PASSWORD}\"}"
|
||||
networks:
|
||||
- elk
|
||||
|
||||
kibana:
|
||||
image: kibana:8.19.19
|
||||
image: kibana:${ELK_VERSION}
|
||||
container_name: kibana
|
||||
depends_on:
|
||||
kibana-setup:
|
||||
condition: service_completed_successfully
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- ELASTICSEARCH_HOSTS=http://elasticsearch:9200
|
||||
- ELASTICSEARCH_USERNAME=kibana_system
|
||||
@@ -57,12 +117,125 @@ services:
|
||||
- XPACK_ENCRYPTEDSAVEDOBJECTS_ENCRYPTIONKEY=${KIBANA_ENCRYPTION_KEY}
|
||||
- XPACK_SECURITY_ENCRYPTIONKEY=${KIBANA_ENCRYPTION_KEY}
|
||||
- XPACK_REPORTING_ENCRYPTIONKEY=${KIBANA_ENCRYPTION_KEY}
|
||||
- NODE_OPTIONS=--max-old-space-size=1024
|
||||
mem_limit: 1200m
|
||||
- XPACK_FLEET_ENABLED=true
|
||||
- SERVER_PUBLICBASEURL=https://kibana.home.server
|
||||
- NODE_OPTIONS=--max-old-space-size=896
|
||||
mem_limit: 900m
|
||||
cpus: 1
|
||||
ports:
|
||||
- "5601:5601"
|
||||
- "5601:5601" # keep for now
|
||||
networks:
|
||||
- elk
|
||||
- web_net
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -s http://localhost:5601/api/status | grep -q '\"level\":\"available\"'"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
- "traefik.http.routers.kibana.rule=Host(`kibana.home.server`)"
|
||||
- "traefik.http.routers.kibana.entrypoints=websecure"
|
||||
- "traefik.http.routers.kibana.tls=true"
|
||||
- "traefik.http.routers.kibana.service=kibana"
|
||||
- "traefik.http.routers.kibana.middlewares=security-headers@file"
|
||||
- "traefik.http.routers.kibana-http.rule=Host(`kibana.home.server`)"
|
||||
- "traefik.http.routers.kibana-http.entrypoints=web"
|
||||
- "traefik.http.routers.kibana-http.middlewares=redirect-to-https@file"
|
||||
- "traefik.http.services.kibana.loadbalancer.server.port=5601"
|
||||
|
||||
logstash:
|
||||
image: logstash:${ELK_VERSION}
|
||||
container_name: logstash
|
||||
profiles: ["logstash"]
|
||||
depends_on:
|
||||
elasticsearch:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- LS_JAVA_OPTS=-Xms512m -Xmx512m
|
||||
- ELASTICSEARCH_USERNAME=elastic
|
||||
- ELASTICSEARCH_PASSWORD=${ELASTIC_PASSWORD}
|
||||
mem_limit: 900m
|
||||
cpus: 1
|
||||
volumes:
|
||||
- ./logstash/pipeline:/usr/share/logstash/pipeline:ro
|
||||
- ./logstash/config/logstash.yml:/usr/share/logstash/config/logstash.yml:ro
|
||||
ports:
|
||||
- "5044:5044" # beats input
|
||||
- "9600:9600" # monitoring API
|
||||
networks:
|
||||
- elk
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -s http://localhost:9600 | grep -q ok"]
|
||||
interval: 10s
|
||||
timeout: 10s
|
||||
retries: 30
|
||||
|
||||
fleet-server:
|
||||
image: elastic/elastic-agent:${ELK_VERSION}
|
||||
container_name: fleet-server
|
||||
profiles: ["fleet"]
|
||||
depends_on:
|
||||
kibana:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
# No service token here on purpose — the agent bootstraps Fleet
|
||||
# Server itself (creates the policy, mints its own token) as long
|
||||
# as it can log into Kibana with real credentials. Without these
|
||||
# two it silently falls back to elastic/changeme and fails auth.
|
||||
- ELASTICSEARCH_USERNAME=elastic
|
||||
- ELASTICSEARCH_PASSWORD=${ELASTIC_PASSWORD}
|
||||
- FLEET_SERVER_ENABLE=true
|
||||
- FLEET_SERVER_ELASTICSEARCH_HOST=http://elasticsearch:9200
|
||||
- FLEET_SERVER_INSECURE_HTTP=true
|
||||
- FLEET_SERVER_HOST=0.0.0.0
|
||||
- FLEET_SERVER_PORT=8220
|
||||
- KIBANA_HOST=http://kibana:5601
|
||||
- KIBANA_FLEET_HOST=http://kibana:5601
|
||||
mem_limit: 400m
|
||||
cpus: 0.5
|
||||
volumes:
|
||||
- fleet-server-state:/usr/share/elastic-agent/state
|
||||
ports:
|
||||
- "8220:8220"
|
||||
networks:
|
||||
- elk
|
||||
- web_net
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -sf http://127.0.0.1:8220/api/status | grep -q HEALTHY"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 30
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
- "traefik.http.routers.fleet.rule=Host(`fleet.home.server`)"
|
||||
- "traefik.http.routers.fleet.entrypoints=websecure"
|
||||
- "traefik.http.routers.fleet.tls=true"
|
||||
- "traefik.http.routers.fleet.service=fleet"
|
||||
- "traefik.http.routers.fleet.middlewares=security-headers@file"
|
||||
- "traefik.http.routers.fleet-http.rule=Host(`fleet.home.server`)"
|
||||
- "traefik.http.routers.fleet-http.entrypoints=web"
|
||||
- "traefik.http.routers.fleet-http.middlewares=redirect-to-https@file"
|
||||
- "traefik.http.services.fleet.loadbalancer.server.port=8220"
|
||||
|
||||
# --- Next lesson, not started yet ---
|
||||
# Once you reach filebeat.yml / auditbeat.yml etc, add each beat here as
|
||||
# its own service (profiles: ["beats"]), same pattern as logstash above:
|
||||
# mount its .yml under ./<beat-name>/, point ELASTICSEARCH_HOSTNAME etc
|
||||
# at elasticsearch/kibana over the `elk` network, and give it a named
|
||||
# volume for its data dir. No Traefik labels needed for beats — they're
|
||||
# agents, not web UIs.
|
||||
|
||||
volumes:
|
||||
es-data:
|
||||
fleet-server-state:
|
||||
|
||||
networks:
|
||||
elk:
|
||||
driver: bridge
|
||||
web_net:
|
||||
external: true
|
||||
|
||||
24
docker-compose/ELK/logstash/pipeline/logstash.conf
Normal file
24
docker-compose/ELK/logstash/pipeline/logstash.conf
Normal file
@@ -0,0 +1,24 @@
|
||||
input {
|
||||
beats {
|
||||
port => 5044
|
||||
}
|
||||
}
|
||||
|
||||
filter {
|
||||
# Add your grok/mutate/etc filters here as you get to that lesson.
|
||||
}
|
||||
|
||||
output {
|
||||
elasticsearch {
|
||||
hosts => ["http://elasticsearch:9200"]
|
||||
user => "elastic"
|
||||
password => "${ELASTICSEARCH_PASSWORD}"
|
||||
index => "logstash-%{+YYYY.MM.dd}"
|
||||
}
|
||||
|
||||
# Comment this out once you trust the pipeline — it's just here so you
|
||||
# can see events flowing while you're learning.
|
||||
stdout {
|
||||
codec => rubydebug
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,28 @@
|
||||
services:
|
||||
semaphore:
|
||||
image: semaphoreui/semaphore:latest
|
||||
container_name: semaphore
|
||||
ports:
|
||||
- "3001:3000"
|
||||
environment:
|
||||
SEMAPHORE_DB_DIALECT: sqlite
|
||||
SEMAPHORE_DB_PATH: /var/lib/semaphore/database.db
|
||||
SEMAPHORE_ADMIN: admin
|
||||
SEMAPHORE_ADMIN_PASSWORD: rezayaghobi1386
|
||||
SEMAPHORE_ADMIN_EMAIL: rezay5639@gmail.com
|
||||
SEMAPHORE_ADMIN_NAME: Admin
|
||||
SEMAPHORE_SECRET_KEY: cc4b3a5a90d31b963bee48a9dee089472bef5e0461f934db7891fc68161d36f7
|
||||
dockscope:
|
||||
image: ghcr.io/manuelr-t/dockscope:latest
|
||||
container_name: dockscope
|
||||
volumes:
|
||||
- ./data:/var/lib/semaphore
|
||||
- ./playbooks:/playbooks
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
# HTTPS
|
||||
- "traefik.http.routers.dockscope.rule=Host(`dockscope.home.server`)"
|
||||
- "traefik.http.routers.dockscope.entrypoints=websecure"
|
||||
- "traefik.http.routers.dockscope.tls=true"
|
||||
- "traefik.http.routers.dockscope.service=dockscope"
|
||||
- "traefik.http.routers.dockscope.middlewares=security-headers@file,dockscope-auth@file"
|
||||
# HTTP → HTTPS redirect
|
||||
- "traefik.http.routers.dockscope-http.rule=Host(`dockscope.home.server`)"
|
||||
- "traefik.http.routers.dockscope-http.entrypoints=web"
|
||||
- "traefik.http.routers.dockscope-http.middlewares=redirect-to-https@file"
|
||||
# Internal port
|
||||
- "traefik.http.services.dockscope.loadbalancer.server.port=4681"
|
||||
|
||||
networks:
|
||||
web_net:
|
||||
external: true
|
||||
@@ -2,18 +2,41 @@ services:
|
||||
filebrowser:
|
||||
image: filebrowser/filebrowser:latest
|
||||
container_name: filebrowser
|
||||
ports:
|
||||
- "8085:80"
|
||||
restart: unless-stopped
|
||||
user: "1000:1000"
|
||||
ports:
|
||||
- "8085:80" # keep for now (fallback)
|
||||
volumes:
|
||||
- /srv/samba/games:/games
|
||||
- /mnt/storage:/storage
|
||||
- /home/rezayaghobi/docker/filebrowser/config:/config
|
||||
- filebrowser_db:/database
|
||||
- filebrowser_srv:/srv
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
|
||||
# HTTPS
|
||||
- "traefik.http.routers.filebrowser.rule=Host(`filebrowser.home.server`)"
|
||||
- "traefik.http.routers.filebrowser.entrypoints=websecure"
|
||||
- "traefik.http.routers.filebrowser.tls=true"
|
||||
- "traefik.http.routers.filebrowser.service=filebrowser"
|
||||
- "traefik.http.routers.filebrowser.middlewares=security-headers@file"
|
||||
|
||||
# HTTP → HTTPS redirect
|
||||
- "traefik.http.routers.filebrowser-http.rule=Host(`filebrowser.home.server`)"
|
||||
- "traefik.http.routers.filebrowser-http.entrypoints=web"
|
||||
- "traefik.http.routers.filebrowser-http.middlewares=redirect-to-https@file"
|
||||
|
||||
# Internal port (Filebrowser listens on 80 inside the container)
|
||||
- "traefik.http.services.filebrowser.loadbalancer.server.port=80"
|
||||
|
||||
volumes:
|
||||
filebrowser_db:
|
||||
filebrowser_srv:
|
||||
|
||||
networks:
|
||||
web_net:
|
||||
external: true
|
||||
|
||||
@@ -5,9 +5,7 @@ services:
|
||||
restart: unless-stopped
|
||||
hostname: gitlab.local
|
||||
ports:
|
||||
- "8929:80"
|
||||
- "2224:22"
|
||||
|
||||
volumes:
|
||||
- ./config:/etc/gitlab
|
||||
- ./logs:/var/log/gitlab
|
||||
@@ -16,10 +14,14 @@ services:
|
||||
environment:
|
||||
GITLAB_OMNIBUS_CONFIG: |
|
||||
gitlab_rails['monitoring_whitelist'] = ['127.0.0.1', '10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
|
||||
external_url 'http://192.168.100.6:8929'
|
||||
external_url 'https://gitlab.home.server'
|
||||
|
||||
nginx['listen_port'] = 80
|
||||
nginx['listen_https'] = false
|
||||
nginx['proxy_set_headers'] = {
|
||||
"X-Forwarded-Proto" => "https",
|
||||
"X-Forwarded-Ssl" => "on"
|
||||
}
|
||||
|
||||
gitlab_rails['gitlab_shell_ssh_port'] = 2224
|
||||
|
||||
@@ -38,14 +40,52 @@ services:
|
||||
postgres_exporter['enable'] = false
|
||||
gitlab_exporter['enable'] = false
|
||||
|
||||
# Registry off for now — flip to true later if you want it
|
||||
registry['enable'] = false
|
||||
# Registry
|
||||
registry['enable'] = true
|
||||
registry_external_url 'https://registry.home.server'
|
||||
gitlab_rails['registry_enabled'] = true
|
||||
gitlab_rails['registry_host'] = "registry.home.server"
|
||||
gitlab_rails['registry_port'] = "443"
|
||||
registry_nginx['listen_port'] = 5050
|
||||
registry_nginx['listen_https'] = false
|
||||
networks:
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
|
||||
# --- Web UI ---
|
||||
- "traefik.http.routers.gitlab.rule=Host(`gitlab.home.server`)"
|
||||
- "traefik.http.routers.gitlab.entrypoints=websecure"
|
||||
- "traefik.http.routers.gitlab.tls=true"
|
||||
- "traefik.http.routers.gitlab.service=gitlab"
|
||||
- "traefik.http.routers.gitlab.middlewares=security-headers@file"
|
||||
- "traefik.http.routers.gitlab-http.rule=Host(`gitlab.home.server`)"
|
||||
- "traefik.http.routers.gitlab-http.entrypoints=web"
|
||||
- "traefik.http.routers.gitlab-http.service=gitlab"
|
||||
- "traefik.http.routers.gitlab-http.middlewares=redirect-to-https@file"
|
||||
- "traefik.http.services.gitlab.loadbalancer.server.port=80"
|
||||
|
||||
# --- Container Registry ---
|
||||
- "traefik.http.routers.registry.rule=Host(`registry.home.server`)"
|
||||
- "traefik.http.routers.registry.entrypoints=websecure"
|
||||
- "traefik.http.routers.registry.tls=true"
|
||||
- "traefik.http.routers.registry.service=registry"
|
||||
- "traefik.http.routers.registry.middlewares=security-headers@file"
|
||||
- "traefik.http.routers.registry-http.rule=Host(`registry.home.server`)"
|
||||
- "traefik.http.routers.registry-http.entrypoints=web"
|
||||
- "traefik.http.routers.registry-http.service=registry"
|
||||
- "traefik.http.routers.registry-http.middlewares=redirect-to-https@file"
|
||||
- "traefik.http.services.registry.loadbalancer.server.port=5050"
|
||||
|
||||
gitlab-runner:
|
||||
image: gitlab/gitlab-runner:latest
|
||||
container_name: gitlab-runner
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9252:9252"
|
||||
volumes:
|
||||
- ./runner-config:/etc/gitlab-runner
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
|
||||
networks:
|
||||
web_net:
|
||||
external: true
|
||||
@@ -17,7 +17,25 @@ services:
|
||||
- /mnt/storage/films/downloads:/downloads
|
||||
networks:
|
||||
- mediastack
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
# HTTPS
|
||||
- "traefik.http.routers.qbittorrent.rule=Host(`qbittorrent.home.server`)"
|
||||
- "traefik.http.routers.qbittorrent.entrypoints=websecure"
|
||||
- "traefik.http.routers.qbittorrent.tls=true"
|
||||
- "traefik.http.routers.qbittorrent.service=qbittorrent"
|
||||
- "traefik.http.routers.qbittorrent.middlewares=security-headers@file"
|
||||
# HTTP → HTTPS redirect
|
||||
- "traefik.http.routers.qbittorrent-http.rule=Host(`qbittorrent.home.server`)"
|
||||
- "traefik.http.routers.qbittorrent-http.entrypoints=web"
|
||||
- "traefik.http.routers.qbittorrent-http.middlewares=redirect-to-https@file"
|
||||
# Internal port (qbittorrent WebUI listens on 8080 inside the container)
|
||||
- "traefik.http.services.qbittorrent.loadbalancer.server.port=8080"
|
||||
|
||||
networks:
|
||||
mediastack:
|
||||
external: true
|
||||
web_net:
|
||||
external: true
|
||||
@@ -2,11 +2,12 @@ services:
|
||||
plex:
|
||||
image: lscr.io/linuxserver/plex:latest
|
||||
container_name: plex
|
||||
restart: unless-stopped
|
||||
network_mode: host
|
||||
|
||||
environment:
|
||||
- TZ=Asia/Tehran
|
||||
- PLEX_CLAIM=claim-uRK14oDz86FsyFgNyaBh
|
||||
|
||||
volumes:
|
||||
- /home/rezayaghobi/docker/plex:/config
|
||||
- /mnt/storage:/media
|
||||
restart: unless-stopped
|
||||
|
||||
@@ -13,7 +13,28 @@ services:
|
||||
- ./config:/config
|
||||
networks:
|
||||
- mediastack
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
|
||||
# HTTPS
|
||||
- "traefik.http.routers.prowlarr.rule=Host(`prowlarr.home.server`)"
|
||||
- "traefik.http.routers.prowlarr.entrypoints=websecure"
|
||||
- "traefik.http.routers.prowlarr.tls=true"
|
||||
- "traefik.http.routers.prowlarr.service=prowlarr"
|
||||
- "traefik.http.routers.prowlarr.middlewares=security-headers@file"
|
||||
|
||||
# HTTP → HTTPS redirect
|
||||
- "traefik.http.routers.prowlarr-http.rule=Host(`prowlarr.home.server`)"
|
||||
- "traefik.http.routers.prowlarr-http.entrypoints=web"
|
||||
- "traefik.http.routers.prowlarr-http.middlewares=redirect-to-https@file"
|
||||
|
||||
# Internal port (prowlarr listens on 9696 inside the container)
|
||||
- "traefik.http.services.prowlarr.loadbalancer.server.port=9696"
|
||||
|
||||
networks:
|
||||
mediastack:
|
||||
external: true
|
||||
web_net:
|
||||
external: true
|
||||
|
||||
@@ -10,13 +10,30 @@ services:
|
||||
- ./config:/config
|
||||
- /mnt/storage/films:/media
|
||||
- /mnt/storage/films/downloads:/downloads
|
||||
|
||||
ports:
|
||||
- 7878:7878
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mediastack
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
# HTTPS
|
||||
- "traefik.http.routers.radarr.rule=Host(`radarr.home.server`)"
|
||||
- "traefik.http.routers.radarr.entrypoints=websecure"
|
||||
- "traefik.http.routers.radarr.tls=true"
|
||||
- "traefik.http.routers.radarr.service=radarr"
|
||||
- "traefik.http.routers.radarr.middlewares=security-headers@file"
|
||||
# HTTP → HTTPS redirect
|
||||
- "traefik.http.routers.radarr-http.rule=Host(`radarr.home.server`)"
|
||||
- "traefik.http.routers.radarr-http.entrypoints=web"
|
||||
- "traefik.http.routers.radarr-http.middlewares=redirect-to-https@file"
|
||||
# Internal port (radarr listens on 7878 inside the container)
|
||||
- "traefik.http.services.radarr.loadbalancer.server.port=7878"
|
||||
|
||||
networks:
|
||||
mediastack:
|
||||
external: true
|
||||
web_net:
|
||||
external: true
|
||||
@@ -16,7 +16,29 @@ services:
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mediastack
|
||||
- web_net
|
||||
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
|
||||
# HTTPS
|
||||
- "traefik.http.routers.sonarr.rule=Host(`sonarr.home.server`)"
|
||||
- "traefik.http.routers.sonarr.entrypoints=websecure"
|
||||
- "traefik.http.routers.sonarr.tls=true"
|
||||
- "traefik.http.routers.sonarr.service=sonarr"
|
||||
- "traefik.http.routers.sonarr.middlewares=security-headers@file"
|
||||
|
||||
# HTTP → HTTPS redirect
|
||||
- "traefik.http.routers.sonarr-http.rule=Host(`sonarr.home.server`)"
|
||||
- "traefik.http.routers.sonarr-http.entrypoints=web"
|
||||
- "traefik.http.routers.sonarr-http.middlewares=redirect-to-https@file"
|
||||
|
||||
# Internal port (Filebrowser listens on 8989 inside the container)
|
||||
- "traefik.http.services.sonarr.loadbalancer.server.port=8989"
|
||||
|
||||
networks:
|
||||
mediastack:
|
||||
external: true
|
||||
web_net:
|
||||
external: true
|
||||
|
||||
@@ -15,10 +15,27 @@ services:
|
||||
- /mnt/storage/films/downloads:/downloads
|
||||
- /mnt/storage/adult:/data/whisparr
|
||||
- /mnt/storage/films/downloads/whisparr:/downloads/whisparr # ADD THIS
|
||||
|
||||
networks:
|
||||
- mediastack
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
# HTTPS
|
||||
- "traefik.http.routers.whisparr.rule=Host(`whisparr.home.server`)"
|
||||
- "traefik.http.routers.whisparr.entrypoints=websecure"
|
||||
- "traefik.http.routers.whisparr.tls=true"
|
||||
- "traefik.http.routers.whisparr.service=whisparr"
|
||||
- "traefik.http.routers.whisparr.middlewares=security-headers@file"
|
||||
# HTTP → HTTPS redirect
|
||||
- "traefik.http.routers.whisparr-http.rule=Host(`whisparr.home.server`)"
|
||||
- "traefik.http.routers.whisparr-http.entrypoints=web"
|
||||
- "traefik.http.routers.whisparr-http.middlewares=redirect-to-https@file"
|
||||
# Internal port (whisparr listens on 6969 inside the container)
|
||||
- "traefik.http.services.whisparr.loadbalancer.server.port=6969"
|
||||
|
||||
networks:
|
||||
mediastack:
|
||||
external: true
|
||||
web_net:
|
||||
external: true
|
||||
@@ -3,13 +3,54 @@ services:
|
||||
image: minio/minio:RELEASE.2024-01-18T22-51-28Z-cpuv1
|
||||
container_name: minio
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9500:9000"
|
||||
- "9501:9001"
|
||||
|
||||
environment:
|
||||
MINIO_ROOT_USER: admin
|
||||
MINIO_ROOT_PASSWORD: change this
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
|
||||
MINIO_BROWSER_REDIRECT_URL: https://minio.home.server
|
||||
|
||||
volumes:
|
||||
- ./data:/data
|
||||
|
||||
command: server /data --console-address ":9001"
|
||||
|
||||
mem_limit: 256m
|
||||
|
||||
networks:
|
||||
- web_net
|
||||
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
|
||||
# Console
|
||||
- "traefik.http.routers.minio-console.rule=Host(`minio.home.server`)"
|
||||
- "traefik.http.routers.minio-console.entrypoints=websecure"
|
||||
- "traefik.http.routers.minio-console.tls=true"
|
||||
- "traefik.http.routers.minio-console.service=minio-console"
|
||||
- "traefik.http.routers.minio-console.middlewares=security-headers@file"
|
||||
- "traefik.http.services.minio-console.loadbalancer.server.port=9001"
|
||||
|
||||
# HTTP -> HTTPS
|
||||
- "traefik.http.routers.minio-console-http.rule=Host(`minio.home.server`)"
|
||||
- "traefik.http.routers.minio-console-http.entrypoints=web"
|
||||
- "traefik.http.routers.minio-console-http.middlewares=redirect-to-https@file"
|
||||
- "traefik.http.routers.minio-console-http.service=minio-console"
|
||||
|
||||
# S3 API
|
||||
- "traefik.http.routers.minio-api.rule=Host(`s3.home.server`)"
|
||||
- "traefik.http.routers.minio-api.entrypoints=websecure"
|
||||
- "traefik.http.routers.minio-api.tls=true"
|
||||
- "traefik.http.routers.minio-api.service=minio-api"
|
||||
- "traefik.http.routers.minio-api.middlewares=security-headers@file"
|
||||
- "traefik.http.services.minio-api.loadbalancer.server.port=9000"
|
||||
|
||||
# HTTP -> HTTPS
|
||||
- "traefik.http.routers.minio-api-http.rule=Host(`s3.home.server`)"
|
||||
- "traefik.http.routers.minio-api-http.entrypoints=web"
|
||||
- "traefik.http.routers.minio-api-http.middlewares=redirect-to-https@file"
|
||||
- "traefik.http.routers.minio-api-http.service=minio-api"
|
||||
|
||||
networks:
|
||||
web_net:
|
||||
external: true
|
||||
@@ -4,7 +4,7 @@ services:
|
||||
container_name: prometheus
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9090:9090"
|
||||
- "9090:9090" # keep for now
|
||||
volumes:
|
||||
- ./prometheus:/etc/prometheus:ro
|
||||
- ./data/prometheus:/prometheus
|
||||
@@ -14,18 +14,44 @@ services:
|
||||
- "--web.enable-lifecycle"
|
||||
networks:
|
||||
- monitoring
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
- "traefik.http.routers.prometheus.rule=Host(`prometheus.home.server`)"
|
||||
- "traefik.http.routers.prometheus.entrypoints=websecure"
|
||||
- "traefik.http.routers.prometheus.tls=true"
|
||||
- "traefik.http.routers.prometheus.service=prometheus"
|
||||
- "traefik.http.routers.prometheus.middlewares=security-headers@file"
|
||||
- "traefik.http.routers.prometheus-http.rule=Host(`prometheus.home.server`)"
|
||||
- "traefik.http.routers.prometheus-http.entrypoints=web"
|
||||
- "traefik.http.routers.prometheus-http.middlewares=redirect-to-https@file"
|
||||
- "traefik.http.services.prometheus.loadbalancer.server.port=9090"
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "3000:3000" # keep for now
|
||||
volumes:
|
||||
- ./grafana/data:/var/lib/grafana
|
||||
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources:ro
|
||||
networks:
|
||||
- monitoring
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
- "traefik.http.routers.grafana.rule=Host(`grafana.home.server`)"
|
||||
- "traefik.http.routers.grafana.entrypoints=websecure"
|
||||
- "traefik.http.routers.grafana.tls=true"
|
||||
- "traefik.http.routers.grafana.service=grafana"
|
||||
- "traefik.http.routers.grafana.middlewares=security-headers@file"
|
||||
- "traefik.http.routers.grafana-http.rule=Host(`grafana.home.server`)"
|
||||
- "traefik.http.routers.grafana-http.entrypoints=web"
|
||||
- "traefik.http.routers.grafana-http.middlewares=redirect-to-https@file"
|
||||
- "traefik.http.services.grafana.loadbalancer.server.port=3000"
|
||||
|
||||
node_exporter:
|
||||
image: prom/node-exporter:latest
|
||||
@@ -39,13 +65,14 @@ services:
|
||||
- /:/rootfs:ro
|
||||
networks:
|
||||
- monitoring
|
||||
# no Traefik labels – no web UI needed
|
||||
|
||||
cadvisor:
|
||||
image: ghcr.io/google/cadvisor:latest
|
||||
container_name: cadvisor
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "8080:8080"
|
||||
- "8080:8080" # keep for now
|
||||
volumes:
|
||||
- /:/rootfs:ro
|
||||
- /var/run:/var/run:rw
|
||||
@@ -53,6 +80,19 @@ services:
|
||||
- /var/lib/docker/:/var/lib/docker:ro
|
||||
networks:
|
||||
- monitoring
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
- "traefik.http.routers.cadvisor.rule=Host(`cadvisor.home.server`)"
|
||||
- "traefik.http.routers.cadvisor.entrypoints=websecure"
|
||||
- "traefik.http.routers.cadvisor.tls=true"
|
||||
- "traefik.http.routers.cadvisor.service=cadvisor"
|
||||
- "traefik.http.routers.cadvisor.middlewares=security-headers@file"
|
||||
- "traefik.http.routers.cadvisor-http.rule=Host(`cadvisor.home.server`)"
|
||||
- "traefik.http.routers.cadvisor-http.entrypoints=web"
|
||||
- "traefik.http.routers.cadvisor-http.middlewares=redirect-to-https@file"
|
||||
- "traefik.http.services.cadvisor.loadbalancer.server.port=8080"
|
||||
|
||||
loki:
|
||||
image: grafana/loki:3.7.3
|
||||
@@ -62,12 +102,25 @@ services:
|
||||
ports:
|
||||
- "3100:3100"
|
||||
volumes:
|
||||
- ./loki/local-config.yaml:/etc/loki/local-config.yaml:ro
|
||||
- ./loki/local-config.yaml:/etc/loki/local-config.yaml:ro
|
||||
- ./data/loki:/loki
|
||||
mem_limit: 400m
|
||||
cpus: 1
|
||||
networks:
|
||||
- monitoring
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
- "traefik.http.routers.loki.rule=Host(`loki.home.server`)"
|
||||
- "traefik.http.routers.loki.entrypoints=websecure"
|
||||
- "traefik.http.routers.loki.tls=true"
|
||||
- "traefik.http.routers.loki.service=loki"
|
||||
- "traefik.http.routers.loki.middlewares=security-headers@file"
|
||||
- "traefik.http.routers.loki-http.rule=Host(`loki.home.server`)"
|
||||
- "traefik.http.routers.loki-http.entrypoints=web"
|
||||
- "traefik.http.routers.loki-http.middlewares=redirect-to-https@file"
|
||||
- "traefik.http.services.loki.loadbalancer.server.port=3100"
|
||||
|
||||
alloy:
|
||||
image: grafana/alloy:v1.17.1
|
||||
@@ -91,7 +144,22 @@ services:
|
||||
- loki
|
||||
networks:
|
||||
- monitoring
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
- "traefik.http.routers.alloy.rule=Host(`alloy.home.server`)"
|
||||
- "traefik.http.routers.alloy.entrypoints=websecure"
|
||||
- "traefik.http.routers.alloy.tls=true"
|
||||
- "traefik.http.routers.alloy.service=alloy"
|
||||
- "traefik.http.routers.alloy.middlewares=security-headers@file"
|
||||
- "traefik.http.routers.alloy-http.rule=Host(`alloy.home.server`)"
|
||||
- "traefik.http.routers.alloy-http.entrypoints=web"
|
||||
- "traefik.http.routers.alloy-http.middlewares=redirect-to-https@file"
|
||||
- "traefik.http.services.alloy.loadbalancer.server.port=12345"
|
||||
|
||||
networks:
|
||||
monitoring:
|
||||
driver: bridge
|
||||
web_net:
|
||||
external: true
|
||||
|
||||
@@ -36,23 +36,54 @@ services:
|
||||
DB_POSTGRESDB_USER: ${POSTGRES_USER}
|
||||
DB_POSTGRESDB_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
|
||||
NPM_ADMIN_EMAIL: ${NPM_ADMIN_EMAIL}
|
||||
NPM_ADMIN_PASSWORD: ${NPM_ADMIN_PASSWORD}
|
||||
PIHOLE_PASSWORD: ${PIHOLE_PASSWORD}
|
||||
|
||||
N8N_BASIC_AUTH_ACTIVE: true
|
||||
N8N_BLOCK_ENV_ACCESS_IN_NODE: false
|
||||
N8N_BASIC_AUTH_USER: ${N8N_BASIC_AUTH_USER}
|
||||
N8N_BASIC_AUTH_PASSWORD: ${N8N_BASIC_AUTH_PASSWORD}
|
||||
|
||||
N8N_ENCRYPTION_KEY: ${N8N_ENCRYPTION_KEY}
|
||||
|
||||
N8N_SECURE_COOKIE: false
|
||||
N8N_HOST: ${N8N_HOST}
|
||||
N8N_HOST: n8n.home.server
|
||||
N8N_PROTOCOL: https
|
||||
N8N_PORT: 5678
|
||||
N8N_PROTOCOL: http
|
||||
WEBHOOK_URL: https://n8n.home.server/
|
||||
|
||||
TZ: ${TZ}
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
|
||||
# HTTPS router
|
||||
- "traefik.http.routers.n8n.rule=Host(`n8n.home.server`)"
|
||||
- "traefik.http.routers.n8n.entrypoints=websecure"
|
||||
- "traefik.http.routers.n8n.tls=true"
|
||||
- "traefik.http.routers.n8n.service=n8n"
|
||||
- "traefik.http.routers.n8n.middlewares=security-headers@file"
|
||||
|
||||
# HTTP → HTTPS redirect
|
||||
- "traefik.http.routers.n8n-http.rule=Host(`n8n.home.server`)"
|
||||
- "traefik.http.routers.n8n-http.entrypoints=web"
|
||||
- "traefik.http.routers.n8n-http.middlewares=redirect-to-https@file"
|
||||
|
||||
# Internal container port
|
||||
- "traefik.http.services.n8n.loadbalancer.server.port=5678"
|
||||
|
||||
ports:
|
||||
- "5678:5678"
|
||||
|
||||
networks:
|
||||
- default # keep the existing n8n_default (for postgres)
|
||||
- web_net # new
|
||||
|
||||
volumes:
|
||||
- ./n8n-data:/home/node/.n8n
|
||||
- /mnt/storage:/mnt/storage
|
||||
- /mnt/windows-video:/mnt/windows-video
|
||||
networks:
|
||||
web_net:
|
||||
external: true
|
||||
|
||||
@@ -10,14 +10,19 @@ services:
|
||||
- ./projects:/home/openhands/workspace/project
|
||||
stdin_open: true
|
||||
tty: true
|
||||
ollama:
|
||||
image: ollama/ollama:latest
|
||||
container_name: ollama
|
||||
depends_on:
|
||||
- github-mcp
|
||||
|
||||
github-mcp:
|
||||
image: ghcr.io/github/github-mcp-server:latest
|
||||
container_name: github-mcp
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "11434:11434"
|
||||
volumes:
|
||||
- ollama-data:/root/.ollama
|
||||
- "8083:8083"
|
||||
stdin_open: true
|
||||
tty: true
|
||||
environment:
|
||||
- GITHUB_PERSONAL_ACCESS_TOKEN=${GITHUB_PERSONAL_ACCESS_TOKEN}
|
||||
|
||||
volumes:
|
||||
openhands-data:
|
||||
ollama-data:
|
||||
@@ -1 +1 @@
|
||||
PIHOLE_WEBPASSWORD=change-me
|
||||
PIHOLE_WEBPASSWORD=CHANGE_ME
|
||||
|
||||
@@ -3,11 +3,10 @@ services:
|
||||
container_name: pihole
|
||||
image: ghcr.io/pi-hole/pihole:latest
|
||||
network_mode: host
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
TZ: "Asia/Tehran"
|
||||
WEBPASSWORD: ${PIHOLE_WEBPASSWORD}
|
||||
FTLCONF_webserver_port: '8082o,[::]:8082o'
|
||||
volumes:
|
||||
- ./etc-pihole:/etc/pihole
|
||||
- ./etc-dnsmasq.d:/etc/dnsmasq.d
|
||||
|
||||
@@ -4,7 +4,31 @@ services:
|
||||
container_name: portainer
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9000" # keep for now (fallback)
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
- /home/rezayaghobi/docker/portainer/data:/data
|
||||
networks:
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
|
||||
# HTTPS
|
||||
- "traefik.http.routers.portainer.rule=Host(`portainer.home.server`)"
|
||||
- "traefik.http.routers.portainer.entrypoints=websecure"
|
||||
- "traefik.http.routers.portainer.tls=true"
|
||||
- "traefik.http.routers.portainer.service=portainer"
|
||||
- "traefik.http.routers.portainer.middlewares=security-headers@file"
|
||||
|
||||
# HTTP → HTTPS redirect
|
||||
- "traefik.http.routers.portainer-http.rule=Host(`portainer.home.server`)"
|
||||
- "traefik.http.routers.portainer-http.entrypoints=web"
|
||||
- "traefik.http.routers.portainer-http.middlewares=redirect-to-https@file"
|
||||
|
||||
# Internal port (Portainer listens on 9000 inside the container)
|
||||
- "traefik.http.services.portainer.loadbalancer.server.port=9000"
|
||||
|
||||
networks:
|
||||
web_net:
|
||||
external: true
|
||||
|
||||
3
docker-compose/semaphore/.env.example
Normal file
3
docker-compose/semaphore/.env.example
Normal file
@@ -0,0 +1,3 @@
|
||||
SEMAPHORE_ADMIN_PASSWORD=CHANGE_ME
|
||||
SEMAPHORE_ADMIN_EMAIL=your-email@example.com
|
||||
SEMAPHORE_SECRET_KEY=CHANGE_ME_generate_with_openssl_rand_hex_32
|
||||
@@ -8,10 +8,32 @@ services:
|
||||
SEMAPHORE_DB_DIALECT: sqlite
|
||||
SEMAPHORE_DB_PATH: /var/lib/semaphore/database.db
|
||||
SEMAPHORE_ADMIN: admin
|
||||
SEMAPHORE_ADMIN_PASSWORD: choose a passwor or use .env (better )
|
||||
SEMAPHORE_ADMIN_EMAIL: rezay5639@gmail.com
|
||||
SEMAPHORE_ADMIN_PASSWORD: ${SEMAPHORE_ADMIN_PASSWORD}
|
||||
SEMAPHORE_ADMIN_EMAIL: ${SEMAPHORE_ADMIN_EMAIL}
|
||||
SEMAPHORE_ADMIN_NAME: Admin
|
||||
SEMAPHORE_SECRET_KEY: <your-long-secret-key>
|
||||
SEMAPHORE_SECRET_KEY: ${SEMAPHORE_SECRET_KEY}
|
||||
volumes:
|
||||
- ./data:/var/lib/semaphore
|
||||
- ./playbooks:/playbooks
|
||||
networks:
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
# HTTPS
|
||||
- "traefik.http.routers.semaphore.rule=Host(`semaphore.home.server`)"
|
||||
- "traefik.http.routers.semaphore.entrypoints=websecure"
|
||||
- "traefik.http.routers.semaphore.tls=true"
|
||||
- "traefik.http.routers.semaphore.service=semaphore"
|
||||
- "traefik.http.routers.semaphore.middlewares=security-headers@file"
|
||||
# HTTP → HTTPS redirect
|
||||
- "traefik.http.routers.semaphore-http.rule=Host(`semaphore.home.server`)"
|
||||
- "traefik.http.routers.semaphore-http.entrypoints=web"
|
||||
- "traefik.http.routers.semaphore-http.middlewares=redirect-to-https@file"
|
||||
# Internal port (semaphore listens on 3000 inside the container)
|
||||
- "traefik.http.services.semaphore.loadbalancer.server.port=3000"
|
||||
restart: unless-stopped
|
||||
|
||||
networks:
|
||||
web_net:
|
||||
external: true
|
||||
|
||||
@@ -6,7 +6,5 @@ services:
|
||||
volumes:
|
||||
- /dev:/dev
|
||||
- /run/udev:/run/udev:ro
|
||||
ports:
|
||||
- "8090:8090"
|
||||
restart: unless-stopped
|
||||
|
||||
- ./report:/etc/hdsentinel
|
||||
restart: unless-stopped
|
||||
134
docker-compose/traefik/README.md
Normal file
134
docker-compose/traefik/README.md
Normal file
@@ -0,0 +1,134 @@
|
||||
# Traefik
|
||||
|
||||
Self-hosted reverse proxy replacing Nginx Proxy Manager — gives every service a friendly `<service>.home.server` hostname with locally-trusted HTTPS, configured entirely as code.
|
||||
|
||||
## Why the migration from Nginx Proxy Manager
|
||||
|
||||
NPM works fine, but the switching cost grew as the homelab did. Two things pushed the move:
|
||||
|
||||
### 1. Infrastructure as code
|
||||
|
||||
NPM's entire configuration lives behind a web UI — proxy hosts, SSL certificates, custom locations, access rules. None of it is version-controlled, exportable, or reproducible. If the container dies and comes back without its data volume, you're rebuilding every proxy host by hand.
|
||||
|
||||
Traefik is the opposite: every route, middleware, TLS cert, and entrypoint is defined in YAML files that live in this repo. The compose file, the `dynamic/` configs, the cert files — all version-controlled, all diffable, all reproducible with `docker compose up -d`. This is the same pattern the rest of the homelab already follows (each service is a Compose file in a folder), and Traefik fits that model naturally where NPM didn't.
|
||||
|
||||
### 2. Docker daemon auto-discovery
|
||||
|
||||
NPM requires you to manually add each proxy host through its web UI — type the domain, point it at the container's IP and port, attach an SSL cert, save. Every new service means another round-trip through the UI.
|
||||
|
||||
Traefik talks directly to the Docker daemon and watches for containers with `traefik.enable=true`. Add a few labels to any compose file and Traefik picks up the new route automatically — no UI interaction, no restart, no manual configuration. This means adding a new service to the reverse proxy is a two-line addition to that service's compose file, not a separate step in a different tool.
|
||||
|
||||
## How it works
|
||||
|
||||
```
|
||||
traefik/
|
||||
├── compose.yaml # Traefik container + static config
|
||||
├── certs/ # Local TLS certificates (mkcert)
|
||||
│ ├── home.server.crt
|
||||
│ └── home.server.key
|
||||
└── dynamic/ # File-based provider — routes, middlewares, TLS
|
||||
├── tls.yaml # Default TLS cert store
|
||||
├── middlewares.yaml # HTTPS redirect + security headers
|
||||
├── pihole.yaml # Pi-hole route
|
||||
├── plex.yml # Plex route (external host)
|
||||
├── sentinel.yml # Sentinel route (external host)
|
||||
└── dockscope-auth.yml # Basic auth middleware for DockScope
|
||||
```
|
||||
|
||||
### Static config (`compose.yaml`)
|
||||
|
||||
Traefik's static configuration is passed as command-line flags:
|
||||
|
||||
- **Docker provider** — watches the Docker daemon for labeled containers on the `web_net` network
|
||||
- **File provider** — watches `/dynamic/` for YAML config changes (hot-reloaded, no restart needed)
|
||||
- **Entrypoints** — `:80` (HTTP) and `:443` (HTTPS)
|
||||
- **API dashboard** — enabled but not insecure (accessed via its own HTTPS route)
|
||||
|
||||
### Dynamic config (`dynamic/`)
|
||||
|
||||
Routes and middlewares are defined as YAML files, not labels. This keeps the compose files clean and makes it easy to see all routing rules in one place:
|
||||
|
||||
- **`middlewares.yaml`** — shared `redirect-to-https` and `security-headers` middlewares (HSTS, XSS filter, content-type nosniff, SAMEORIGIN framing)
|
||||
- **`tls.yaml`** — default TLS cert store pointing at the mkcert-generated certificate
|
||||
- **`pihole.yaml`**, **`plex.yml`**, **`sentinel.yml`** — per-service route definitions for services that can't use Docker labels (host-network or external hosts)
|
||||
|
||||
### Services using Docker labels
|
||||
|
||||
Most services define their Traefik routes directly in their compose files via labels:
|
||||
|
||||
```yaml
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
- "traefik.http.routers.<service>.rule=Host(`<service>.home.server`)"
|
||||
- "traefik.http.routers.<service>.entrypoints=websecure"
|
||||
- "traefik.http.routers.<service>.tls=true"
|
||||
- "traefik.http.routers.<service>.middlewares=security-headers@file"
|
||||
- "traefik.http.routers.<service>-http.rule=Host(`<service>.home.server`)"
|
||||
- "traefik.http.routers.<service>-http.entrypoints=web"
|
||||
- "traefik.http.routers.<service>-http.middlewares=redirect-to-https@file"
|
||||
- "traefik.http.services.<service>.loadbalancer.server.port=<internal-port>"
|
||||
```
|
||||
|
||||
Services currently fronted by Traefik:
|
||||
|
||||
| Service | Hostname | Internal Port |
|
||||
|---|---|---|
|
||||
| Prometheus | `prometheus.home.server` | 9090 |
|
||||
| Grafana | `grafana.home.server` | 3000 |
|
||||
| cAdvisor | `cadvisor.home.server` | 8080 |
|
||||
| Loki | `loki.home.server` | 3100 |
|
||||
| Alloy | `alloy.home.server` | 12345 |
|
||||
| Pi-hole | `pihole.home.server` | 8082 (host) |
|
||||
| Portainer | `portainer.home.server` | 9000 |
|
||||
| n8n | `n8n.home.server` | 5678 |
|
||||
| Filebrowser | `filebrowser.home.server` | 80 |
|
||||
| DockScope | `dockscope.home.server` | 4681 |
|
||||
| Semaphore | `semaphore.home.server` | 3000 |
|
||||
| MinIO | `minio.home.server` | 9001 |
|
||||
| Plex | `plex.home.server` | 32400 (external) |
|
||||
| Sentinel | `sentinel.home.server` | 8088 (external) |
|
||||
| Vaultwarden | `vaultwarden.home.server` | 80 |
|
||||
| Traefik Dashboard | `traefik.home.server` | API@internal |
|
||||
|
||||
## TLS certificates
|
||||
|
||||
Same approach as before — [mkcert](https://github.com/FiloSottile/mkcert) generates a local CA and signs certificates for `*.home.server`. The cert and key live in `certs/` and are mounted read-only into the Traefik container.
|
||||
|
||||
Traefik loads the default cert via `dynamic/tls.yaml`:
|
||||
|
||||
```yaml
|
||||
tls:
|
||||
stores:
|
||||
default:
|
||||
defaultCertificate:
|
||||
certFile: /certs/home.server.crt
|
||||
keyFile: /certs/home.server.key
|
||||
```
|
||||
|
||||
Adding a new service with a `.home.server` hostname automatically gets the right cert — no per-host certificate assignment needed.
|
||||
|
||||
## Adding a new service
|
||||
|
||||
1. **If the service is a Docker container on `web_net`** — add Traefik labels to its compose file (copy the pattern from any existing service above)
|
||||
2. **If the service is host-network or external** — create a new YAML file in `dynamic/` with the router and service definition
|
||||
3. **Add the hostname to Pi-hole** — so DNS resolves `<service>.home.server` to the server's LAN IP
|
||||
|
||||
No restart required — Traefik watches both the Docker daemon and the `dynamic/` directory and picks up changes automatically.
|
||||
|
||||
## Network
|
||||
|
||||
All Traefik-fronted services join the `web_net` external Docker network. This network must be created before starting Traefik:
|
||||
|
||||
```bash
|
||||
docker network create web_net
|
||||
```
|
||||
|
||||
## Security notes
|
||||
|
||||
- The Traefik dashboard is behind its own HTTPS route with security headers — not exposed on the public internet, LAN-only via Pi-hole DNS
|
||||
- DockScope additionally uses a basicAuth middleware (`dynamic/dockscope-auth.yml`) since it mounts the Docker socket
|
||||
- All HTTP requests are redirected to HTTPS permanently (301)
|
||||
- Security headers include HSTS (1 year, includeSubdomains, preload), XSS filter, content-type nosniff, and SAMEORIGIN framing
|
||||
|
||||
[← Back to Home](../../docs/Home.md)
|
||||
25
docker-compose/traefik/certs/home.server.crt
Normal file
25
docker-compose/traefik/certs/home.server.crt
Normal file
@@ -0,0 +1,25 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIEQTCCAqmgAwIBAgIQMHgxq9We6lR+AyMJeJHo7DANBgkqhkiG9w0BAQsFADB3
|
||||
MR4wHAYDVQQKExVta2NlcnQgZGV2ZWxvcG1lbnQgQ0ExJjAkBgNVBAsMHXJlemF5
|
||||
YWdob2JpQGhvbWVzZXJ2ZXIgKHJlemEpMS0wKwYDVQQDDCRta2NlcnQgcmV6YXlh
|
||||
Z2hvYmlAaG9tZXNlcnZlciAocmV6YSkwHhcNMjYwODI0MDgzODU5WhcNMjgxMTI0
|
||||
MDgzODU5WjBRMScwJQYDVQQKEx5ta2NlcnQgZGV2ZWxvcG1lbnQgY2VydGlmaWNh
|
||||
dGUxJjAkBgNVBAsMHXJlemF5YWdob2JpQGhvbWVzZXJ2ZXIgKHJlemEpMIIBIjAN
|
||||
BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvgzUWThVUpVqdqkoJ8325vsC5Eoe
|
||||
mNlihoZ+zXenBBpZgvuiMdz+duBGPdWQcXK7DQr0jBXNqx0wt8w4RSg9pBETPNXT
|
||||
mcRxudzzvKggVVzUqJ6MrODj2NSNCxUL05oR/iH30+pmliCRE3CyjEvlnpcUVdu1
|
||||
Qwp0LdxC4fR2ZNbWQbRZ3aoQXYwaQ7WaFno9tlyhvhBdh/rtQOMhi6kVdtB8pTGY
|
||||
Zzc1fSBJsOCqlwyN6jcp/J8jZ3AfCU1fdqZwMXuQd0TbUOv/K1grQ99IkzJu2zKJ
|
||||
jKomBdD1SZnn+/buUl2KGAl1e/G6/LIDgoKVDjb7y4XNCcZX4oNx7pxPdwIDAQAB
|
||||
o28wbTAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwHwYDVR0j
|
||||
BBgwFoAUuWL4Syj7LA309BRwMJjRz3eC+FUwJQYDVR0RBB4wHIINKi5ob21lLnNl
|
||||
cnZlcoILaG9tZS5zZXJ2ZXIwDQYJKoZIhvcNAQELBQADggGBAKmBR+udJASNvgtN
|
||||
LtQzHcq790hnnVg/O1dbSd0Ah2PSrz/OtYOVMgOTa9WPIYeMjVKVOsRcPlR8lO7O
|
||||
udJuDUi3QkSQ+hmsqFKqC9Mt9uPg6ozcGNcX8/jC/eTIJKYlQCdI69brehR/R4rd
|
||||
xQo+1MYv/JUULUy/1Ix6uGs80jNuVGwwEnfTxU0K47FUI7hx5HjR8wHv0cMY3Fb9
|
||||
tUEJJfWjyuyq+oXB3x8loEEBL7ESQUb0pCKKmtmclerm0ia1X5Sh2Sn+d/Me3RHC
|
||||
ABh0zwQcmC0j/r4T5ajTr5ZK5jzo+3NTtqpLF0TiKAPioGr+W9sJlw5IZhlIVpN8
|
||||
UqNrwvUbyiVuNUrl70QabO0G3PFUX/Kfi1tXh+9eletFpz3CBQrrJljOFoXhzVFt
|
||||
FZGPiQuKkAryDvdMpS5oRnOcgR/hROIkz6ACkECwvvbWE5wb7BX+9NZm39gpNe8r
|
||||
5Cj1OyF6+HZ+o5NHOKzcj4Ci0/e0m+I4Fh9kG5ZXC5+cX79z/A==
|
||||
-----END CERTIFICATE-----
|
||||
56
docker-compose/traefik/compose.yaml
Normal file
56
docker-compose/traefik/compose.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
services:
|
||||
traefik:
|
||||
image: traefik:v3.7
|
||||
container_name: traefik
|
||||
restart: unless-stopped
|
||||
|
||||
command:
|
||||
- --log.level=INFO
|
||||
|
||||
# API / dashboard
|
||||
- --api.dashboard=true
|
||||
- --api.insecure=false
|
||||
|
||||
# Docker provider
|
||||
- --providers.docker=true
|
||||
- --providers.docker.exposedbydefault=false
|
||||
- --providers.docker.network=web_net
|
||||
- --providers.file.directory=/dynamic
|
||||
- --providers.file.watch=true
|
||||
|
||||
# Entrypoints
|
||||
- --entrypoints.web.address=:80
|
||||
- --entrypoints.websecure.address=:443
|
||||
|
||||
# Health check
|
||||
- --ping=true
|
||||
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
- ./dynamic:/dynamic:ro
|
||||
- ./certs:/certs:ro
|
||||
networks:
|
||||
- web_net
|
||||
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
|
||||
# Dashboard HTTPS
|
||||
- "traefik.http.routers.traefik.rule=Host(`traefik.home.server`)"
|
||||
- "traefik.http.routers.traefik.entrypoints=websecure"
|
||||
- "traefik.http.routers.traefik.tls=true"
|
||||
- "traefik.http.routers.traefik.service=api@internal"
|
||||
- "traefik.http.routers.traefik.middlewares=security-headers@file"
|
||||
|
||||
# Dashboard HTTP → HTTPS redirect
|
||||
- "traefik.http.routers.traefik-http.rule=Host(`traefik.home.server`)"
|
||||
- "traefik.http.routers.traefik-http.entrypoints=web"
|
||||
- "traefik.http.routers.traefik-http.middlewares=redirect-to-https@file"
|
||||
networks:
|
||||
web_net:
|
||||
external: true
|
||||
7
docker-compose/traefik/dynamic/dockscope-auth.yml
Normal file
7
docker-compose/traefik/dynamic/dockscope-auth.yml
Normal file
@@ -0,0 +1,7 @@
|
||||
http:
|
||||
middlewares:
|
||||
dockscope-auth:
|
||||
basicAuth:
|
||||
users:
|
||||
# Generate with: htpasswd -nB <username>
|
||||
- "${DOCKSCOPE_AUTH_USER}:${DOCKSCOPE_AUTH_HASH}"
|
||||
17
docker-compose/traefik/dynamic/middlewares.yaml
Normal file
17
docker-compose/traefik/dynamic/middlewares.yaml
Normal file
@@ -0,0 +1,17 @@
|
||||
http:
|
||||
middlewares:
|
||||
redirect-to-https:
|
||||
redirectScheme:
|
||||
scheme: https
|
||||
permanent: true
|
||||
|
||||
# Optional but recommended security headers (we can tune later)
|
||||
security-headers:
|
||||
headers:
|
||||
browserXssFilter: true
|
||||
contentTypeNosniff: true
|
||||
forceSTSHeader: true
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: true
|
||||
stsSeconds: 31536000
|
||||
customFrameOptionsValue: "SAMEORIGIN"
|
||||
24
docker-compose/traefik/dynamic/pihole.yaml
Normal file
24
docker-compose/traefik/dynamic/pihole.yaml
Normal file
@@ -0,0 +1,24 @@
|
||||
http:
|
||||
routers:
|
||||
pihole:
|
||||
rule: "Host(`pihole.home.server`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
service: pihole
|
||||
middlewares:
|
||||
- security-headers@file
|
||||
|
||||
pihole-http:
|
||||
rule: "Host(`pihole.home.server`)"
|
||||
entryPoints:
|
||||
- web
|
||||
service: pihole # ← added (required)
|
||||
middlewares:
|
||||
- redirect-to-https@file
|
||||
|
||||
services:
|
||||
pihole:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://172.17.0.1:8082"
|
||||
24
docker-compose/traefik/dynamic/plex.yml
Normal file
24
docker-compose/traefik/dynamic/plex.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
http:
|
||||
routers:
|
||||
plex:
|
||||
rule: "Host(`plex.home.server`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: {}
|
||||
service: plex
|
||||
middlewares:
|
||||
- security-headers@file
|
||||
|
||||
plex-http:
|
||||
rule: "Host(`plex.home.server`)"
|
||||
entryPoints:
|
||||
- web
|
||||
service: plex
|
||||
middlewares:
|
||||
- redirect-to-https@file
|
||||
|
||||
services:
|
||||
plex:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.100.6:32400"
|
||||
24
docker-compose/traefik/dynamic/sentinel.yml
Normal file
24
docker-compose/traefik/dynamic/sentinel.yml
Normal file
@@ -0,0 +1,24 @@
|
||||
http:
|
||||
routers:
|
||||
sentinel:
|
||||
rule: "Host(`sentinel.home.server`)"
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls: true
|
||||
service: sentinel
|
||||
middlewares:
|
||||
- security-headers@file
|
||||
|
||||
sentinel-http:
|
||||
rule: "Host(`sentinel.home.server`)"
|
||||
entryPoints:
|
||||
- web
|
||||
service: sentinel
|
||||
middlewares:
|
||||
- redirect-to-https@file
|
||||
|
||||
services:
|
||||
sentinel:
|
||||
loadBalancer:
|
||||
servers:
|
||||
- url: "http://192.168.100.6:8088"
|
||||
6
docker-compose/traefik/dynamic/tls.yaml
Normal file
6
docker-compose/traefik/dynamic/tls.yaml
Normal file
@@ -0,0 +1,6 @@
|
||||
tls:
|
||||
stores:
|
||||
default:
|
||||
defaultCertificate:
|
||||
certFile: /certs/home.server.crt
|
||||
keyFile: /certs/home.server.key
|
||||
33
docker-compose/vaultwarden/docker-compose.yml
Normal file
33
docker-compose/vaultwarden/docker-compose.yml
Normal file
@@ -0,0 +1,33 @@
|
||||
services:
|
||||
vaultwarden:
|
||||
image: vaultwarden/server:latest
|
||||
container_name: vaultwarden
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- WEBSOCKET_ENABLED=true
|
||||
- SIGNUPS_ALLOWED=false # set false after creating your account
|
||||
volumes:
|
||||
- ./vw-data:/data
|
||||
ports:
|
||||
- "8084:80"
|
||||
networks:
|
||||
- web_net
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.docker.network=web_net"
|
||||
# HTTPS
|
||||
- "traefik.http.routers.vaultwarden.rule=Host(`vaultwarden.home.server`)"
|
||||
- "traefik.http.routers.vaultwarden.entrypoints=websecure"
|
||||
- "traefik.http.routers.vaultwarden.tls=true"
|
||||
- "traefik.http.routers.vaultwarden.service=vaultwarden"
|
||||
- "traefik.http.routers.vaultwarden.middlewares=security-headers@file"
|
||||
# HTTP → HTTPS redirect
|
||||
- "traefik.http.routers.vaultwarden-http.rule=Host(`vaultwarden.home.server`)"
|
||||
- "traefik.http.routers.vaultwarden-http.entrypoints=web"
|
||||
- "traefik.http.routers.vaultwarden-http.middlewares=redirect-to-https@file"
|
||||
# Internal port (vaultwarden listens on 80 inside the container)
|
||||
- "traefik.http.services.vaultwarden.loadbalancer.server.port=80"
|
||||
|
||||
networks:
|
||||
web_net:
|
||||
external: true
|
||||
@@ -13,7 +13,9 @@ Documentation for a self-hosted homelab running entirely on Docker Compose. This
|
||||
- [Storage Health](Storage-Health.md)
|
||||
- [Lessons Learned](Lessons-Learned.md)
|
||||
- [AI Coding Agent](AI-Agent.md)
|
||||
- [Reverse Proxy](Reverse-Proxy.md)
|
||||
- [Reverse Proxy](Reverse-Proxy.md) (historical — Nginx Proxy Manager)
|
||||
- [Traefik](Traefik.md) (current reverse proxy)
|
||||
- [Vaultwarden](Vaultwarden.md) (password manager)
|
||||
- [Semaphore](Semaphore.md)
|
||||
- [DockScope](DockScope.md)
|
||||
- [Ansible Playbooks](AnsiblePlaybooks.md)
|
||||
@@ -30,7 +32,7 @@ Documentation for a self-hosted homelab running entirely on Docker Compose. This
|
||||
| **Orchestration** | Docker Compose, one stack per service |
|
||||
| **Management** | Portainer, **DockScope** (3D visual dashboard) |
|
||||
| **Monitoring** | Prometheus + Grafana + cAdvisor + node-exporter + **Loki + Alloy** (centralized logs) |
|
||||
| **Network** | Pi-hole (DNS filtering), Samba (file sharing), nginx (proxy managing) |
|
||||
| **Network** | Pi-hole (DNS filtering), Samba (file sharing), **Traefik** (reverse proxy, config-as-code) |
|
||||
| **Automation** | n8n + **Semaphore** (Ansible Web UI) + **Ansible** (update playbooks) || **Media** | Plex + Sonarr/Radarr/Prowlarr + qBittorrent |
|
||||
| **Disk health** | Hard Disk Sentinel |
|
||||
| **AI Agent** | OpenHands + Ollama |
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Reverse Proxy
|
||||
# Reverse Proxy (historical — Nginx Proxy Manager)
|
||||
|
||||
[Nginx Proxy Manager](https://nginxproxymanager.com/) sits in front of every service on the LAN, replacing `http://<ip>:<port>` with friendly `<service>.home` names and trusted local HTTPS. Runs as its own Compose stack under `nginx-proxy-manager/`.
|
||||
> **Note:** This homelab has migrated from Nginx Proxy Manager to **Traefik**. See [Traefik](Traefik.md) for the current reverse proxy setup and migration rationale. This page documents the original NPM setup as a reference.
|
||||
|
||||
[Nginx Proxy Manager](https://nginxproxymanager.com/) sat in front of every service on the LAN, replacing `http://<ip>:<port>` with friendly `<service>.home` names and trusted local HTTPS. Ran as its own Compose stack under `nginx-proxy-manager/`.
|
||||
|
||||

|
||||
|
||||
|
||||
129
docs/Traefik.md
Normal file
129
docs/Traefik.md
Normal file
@@ -0,0 +1,129 @@
|
||||
# Traefik — Reverse Proxy
|
||||
|
||||
Traefik replaced Nginx Proxy Manager as the homelab's reverse proxy, giving every service a `<service>.home.server` hostname with locally-trusted HTTPS — now configured entirely as code instead of a web UI.
|
||||
|
||||
See the [Traefik README](../docker-compose/traefik/README.md) for the full file-by-file breakdown and how to add new services.
|
||||
|
||||
## Why the migration
|
||||
|
||||
Two reasons, both rooted in the same observation: NPM works, but it doesn't scale with the rest of the infrastructure-as-code approach.
|
||||
|
||||
**Everything as code.** NPM's configuration lives entirely in its web UI — proxy hosts, SSL certs, custom locations, access rules. None of it is version-controlled or reproducible. If the data volume is lost, you're rebuilding every proxy host manually. Traefik's routes, middlewares, and TLS config are all YAML files in this repo — diffable, reviewable, reproducible with `docker compose up -d`.
|
||||
|
||||
**Docker daemon auto-discovery.** NPM requires manually adding each proxy host through its UI — domain, target IP/port, SSL cert, save. Every new service means another round-trip through a different tool. Traefik watches the Docker daemon directly and picks up containers with `traefik.enable=true` automatically. Adding a new service to the reverse proxy is a two-line label addition to that service's compose file.
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Traefik v3.7 │
|
||||
│ │
|
||||
│ Entrypoints: :80 (HTTP) → redirect to HTTPS │
|
||||
│ :443 (HTTPS) → route to service │
|
||||
│ │
|
||||
│ Providers: │
|
||||
│ Docker daemon → auto-discovers labeled conns │
|
||||
│ File provider → watches dynamic/ for YAML │
|
||||
│ │
|
||||
│ TLS: mkcert local CA, single cert for all hosts │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
│ │ │
|
||||
┌────┴────┐ ┌─────┴─────┐ ┌────┴────┐
|
||||
│ Docker │ │ File │ │ certs/ │
|
||||
│ labels │ │ dynamic/ │ │ mkcert │
|
||||
│ per-svc │ │ routes │ │ CA │
|
||||
└─────────┘ └───────────┘ └─────────┘
|
||||
```
|
||||
|
||||
### Static config
|
||||
|
||||
Passed as command-line flags in `compose.yaml`:
|
||||
|
||||
- **Docker provider** — watches for labeled containers on `web_net`
|
||||
- **File provider** — hot-reloads `dynamic/` directory
|
||||
- **Entrypoints** — `:80` and `:443`
|
||||
- **Ping** — health check endpoint
|
||||
- **API dashboard** — enabled, secured via its own HTTPS route
|
||||
|
||||
### Dynamic config (`dynamic/`)
|
||||
|
||||
| File | Purpose |
|
||||
|---|---|
|
||||
| `tls.yaml` | Default TLS cert store pointing at the mkcert certificate |
|
||||
| `middlewares.yaml` | `redirect-to-https` (301) + `security-headers` (HSTS, XSS, nosniff, SAMEORIGIN) |
|
||||
| `pihole.yaml` | Pi-hole route (host-network, can't use Docker labels) |
|
||||
| `plex.yml` | Plex route (external host `192.168.100.6:32400`) |
|
||||
| `sentinel.yml` | Sentinel route (external host `192.168.100.6:8088`) |
|
||||
| `dockscope-auth.yml` | BasicAuth middleware for DockScope (Docker socket access) |
|
||||
|
||||
### Services fronted by Traefik
|
||||
|
||||
| Service | Hostname | Method |
|
||||
|---|---|---|
|
||||
| Prometheus | `prometheus.home.server` | Docker labels |
|
||||
| Grafana | `grafana.home.server` | Docker labels |
|
||||
| cAdvisor | `cadvisor.home.server` | Docker labels |
|
||||
| Loki | `loki.home.server` | Docker labels |
|
||||
| Alloy | `alloy.home.server` | Docker labels |
|
||||
| Pi-hole | `pihole.home.server` | File provider |
|
||||
| Portainer | `portainer.home.server` | Docker labels |
|
||||
| n8n | `n8n.home.server` | Docker labels |
|
||||
| Filebrowser | `filebrowser.home.server` | Docker labels |
|
||||
| DockScope | `dockscope.home.server` | Docker labels + basicAuth |
|
||||
| Semaphore | `semaphore.home.server` | Docker labels |
|
||||
| MinIO | `minio.home.server` | Docker labels |
|
||||
| Plex | `plex.home.server` | File provider (external) |
|
||||
| Sentinel | `sentinel.home.server` | File provider (external) |
|
||||
| Vaultwarden | `vaultwarden.home.server` | Docker labels |
|
||||
| Traefik Dashboard | `traefik.home.server` | Docker labels |
|
||||
|
||||
## TLS certificates
|
||||
|
||||
Same approach as the NPM era — [mkcert](https://github.com/FiloSottile/mkcert) creates a local CA trusted on each device, then signs a single certificate covering all `*.home.server` hostnames. The cert and key live in `traefik/certs/` and are mounted read-only.
|
||||
|
||||
Traefik's file-based TLS store (`dynamic/tls.yaml`) applies this cert as the default for all routes — no per-host certificate assignment needed. Adding a new `.home.server` hostname automatically gets the right cert.
|
||||
|
||||
## Network
|
||||
|
||||
All Traefik-fronted services join the `web_net` external Docker network:
|
||||
|
||||
```bash
|
||||
docker network create web_net
|
||||
```
|
||||
|
||||
Services that can't join `web_net` (host-network containers like Pi-hole, or external machines like Plex) are routed via the file provider with explicit IP:port targets.
|
||||
|
||||
## Adding a new service
|
||||
|
||||
1. **Docker container on `web_net`** — add Traefik labels to its compose file
|
||||
2. **Host-network or external** — create a YAML file in `dynamic/`
|
||||
3. **Pi-hole** — add DNS record for `<service>.home.server`
|
||||
|
||||
No restart required — Traefik auto-discovers Docker label changes and hot-reloads file provider changes.
|
||||
|
||||
## Port 80/443 conflict with Pi-hole
|
||||
|
||||
Pi-hole runs in `network_mode: host`, which means it binds directly to ports 80 and 443 for its web UI — the same ports Traefik needs. Resolved by moving Pi-hole's web interface to port 8082 via the FTL environment variable:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
FTLCONF_webserver_port: '8082o,[::]:8082o'
|
||||
```
|
||||
|
||||
Pi-hole's DNS service (port 53) is untouched — only its web UI moved. See [Reverse Proxy](Reverse-Proxy.md) for the full history of this port conflict.
|
||||
|
||||
## Security notes
|
||||
|
||||
- Dashboard is LAN-only via Pi-hole DNS (not exposed publicly)
|
||||
- DockScope has basicAuth in addition to HTTPS (Docker socket access)
|
||||
- All HTTP → HTTPS redirects are permanent (301)
|
||||
- Security headers: HSTS (1 year, includeSubdomains, preload), XSS filter, content-type nosniff, SAMEORIGIN framing
|
||||
|
||||
## Lessons learned
|
||||
|
||||
- Traefik's Docker label approach means the reverse proxy configuration is co-located with each service — no separate tool or UI to maintain. This matches the "each service is a folder" philosophy the rest of the homelab already follows.
|
||||
- File-based routes in `dynamic/` are useful for services that can't use Docker labels (host-network containers, external machines). Keeping them as separate YAML files makes it obvious which services use which routing method.
|
||||
- The `web_net` external network is a hard requirement — Traefik can only route to containers on networks it's connected to. Forgetting to add a new service to `web_net` is the most common "why isn't my route working" issue.
|
||||
- Hot-reloading (file provider + Docker provider) means zero-downtime config changes — a major operational improvement over NPM, which required a container restart for some config changes.
|
||||
|
||||
[← Back to Home](Home.md)
|
||||
96
docs/Vaultwarden.md
Normal file
96
docs/Vaultwarden.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Vaultwarden
|
||||
|
||||
Self-hosted password manager — a lightweight, Rust-based implementation of the Bitwarden server API, giving full Bitwarden client compatibility (browser extensions, mobile apps, desktop apps) without Bitwarden's cloud dependency.
|
||||
|
||||
See [docker-compose/vaultwarden/](../docker-compose/vaultwarden/) for the deployment files.
|
||||
|
||||
## Why
|
||||
|
||||
After years of managing passwords across various tools, a self-hosted password manager was the missing piece. Vaultwarden provides:
|
||||
|
||||
- **Full Bitwarden compatibility** — works with the official Bitwarden browser extension, iOS/Android apps, and desktop apps. No proprietary client lock-in.
|
||||
- **Lightweight** — single container, SQLite database, minimal resource usage. Runs comfortably alongside the rest of the stack.
|
||||
- **Local data ownership** — passwords never leave the LAN unless you want them to (via Bitwarden's sync protocol for mobile access outside the home).
|
||||
- **Long passwords** — using Vaultwarden as the source of truth for 30+ character passwords across all services, replacing the short memorable passwords that were previously hardcoded in compose files.
|
||||
|
||||
## Setup
|
||||
|
||||
### Initial configuration
|
||||
|
||||
```bash
|
||||
cd docker-compose/vaultwarden
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Access the web UI at `vaultwarden.home.server` (via Traefik) or `http://localhost:8084` directly.
|
||||
|
||||
**First step:** create your admin account, then set `SIGNUPS_ALLOWED=false` in the compose file to prevent new registrations.
|
||||
|
||||
### Generating password hashes
|
||||
|
||||
Vaultwarden uses the same `admin_token` mechanism as Bitwarden for admin access. For the Traefik basicAuth middleware (DockScope), generate hashes with:
|
||||
|
||||
```bash
|
||||
htpasswd -nB <username>
|
||||
```
|
||||
|
||||
This produces bcrypt-hashed entries compatible with Traefik's basicAuth middleware.
|
||||
|
||||
## Integration with the homelab
|
||||
|
||||
### Traefik routing
|
||||
|
||||
Vaultwarden is fronted by Traefik with the standard label set:
|
||||
|
||||
- `vaultwarden.home.server` → HTTPS → container port 80
|
||||
- HTTP → HTTPS redirect
|
||||
- Security headers middleware
|
||||
|
||||
### Password management workflow
|
||||
|
||||
1. Store all service passwords in Vaultwarden (30+ character randomly generated)
|
||||
2. Reference passwords via `.env` files on the host (not in compose files)
|
||||
3. Compose files use `${VARIABLE}` references, never hardcoded values
|
||||
|
||||
This is the migration path from the previous approach of short, memorable passwords hardcoded in compose files — the Traefik migration was the catalyst for adopting proper secret management across the stack.
|
||||
|
||||
## Configuration
|
||||
|
||||
Key environment variables in `docker-compose.yml`:
|
||||
|
||||
| Variable | Value | Purpose |
|
||||
|---|---|---|
|
||||
| `WEBSOCKET_ENABLED` | `true` | Enable real-time sync with Bitwarden clients |
|
||||
| `SIGNUPS_ALLOWED` | `false` | Disable new account creation after initial setup |
|
||||
|
||||
Data is stored in `./vw-data/` (gitignored).
|
||||
|
||||
## Security notes
|
||||
|
||||
- Signups are disabled after initial account creation
|
||||
- The web UI is only accessible via HTTPS through Traefik (LAN-only via Pi-hole DNS)
|
||||
- Vaultwarden encrypts all vault data client-side before transmission — even if the server is compromised, passwords remain encrypted
|
||||
- The `vw-data/` directory contains the SQLite database and should be backed up regularly
|
||||
- Admin panel is accessible at `vaultwarden.home.server/admin` (requires admin token)
|
||||
|
||||
## Backups
|
||||
|
||||
The SQLite database in `vw-data/` should be backed up as part of the regular backup strategy. A simple approach:
|
||||
|
||||
```bash
|
||||
# stop the container, copy the data, restart
|
||||
docker compose stop vaultwarden
|
||||
cp -r vw-data/ /backups/vaultwarden-$(date +%Y%m%d)/
|
||||
docker compose start vaultwarden
|
||||
```
|
||||
|
||||
Or use the Bitwarden CLI to export a JSON/CSV backup from within the web UI.
|
||||
|
||||
## Lessons learned
|
||||
|
||||
- Migrating from hardcoded passwords to a password manager is painful but worth it — the Traefik migration forced the cleanup, and now every service uses proper secret references.
|
||||
- 30+ character passwords are free with a password manager — there's no reason to use short, memorable passwords for services that only need them in `.env` files.
|
||||
- Vaultwarden's Bitwarden compatibility means you don't have to convince family members to use a different app — the official Bitwarden clients just work.
|
||||
- The `SIGNUPS_ALLOWED` flag is easy to forget — leave it `true` too long and anyone on the LAN can create an account.
|
||||
|
||||
[← Back to Home](Home.md)
|
||||
Reference in New Issue
Block a user