name: hestia-ferron-demo # Demo: HestiaCP (Apache origin) + Ferron 3 (LSCache-compatible caching reverse proxy) # Architecture: # client -> Ferron :80/:443 (edge cache, TLS, rate-limit, observability) # -> Hestia Apache :8080 (Hestia-managed vhosts, .htaccess, PHP-FPM) # Hestia's nginx is stopped/disabled — no Hestia rebuild required. # Ferron uses a catch-all host so new Hestia domains work with zero Ferron reloads # (Host header is preserved, Apache selects the right vhost). services: hestia: build: context: ./hestia dockerfile: Dockerfile args: HESTIA_IMAGE: ${HESTIA_IMAGE:-ghcr.io/ptrinh/hestiacp:latest} image: hestia-ferron-demo/hestia:latest hostname: ${HESTIA_HOSTNAME:-hestia.demo.local} # NOTE: base image (ghcr.io/ptrinh/hestiacp) is non-privileged / no systemd. # Do NOT add `tmpfs: /run` — it wipes /run/lock, breaking `apache2ctl` # (mktemp /var/lock/apache2.XXX fails) so Apache never starts → Ferron 502. # Upstream runs with default caps and no tmpfs; we match that here. volumes: - hestia-home:/home - hestia-data:/usr/local/hestia - hestia-log:/var/log/hestia - ./hestia/demo-www:/opt/demo-www:ro - ./hestia/entrypoint-ferron-demo.sh:/usr/local/bin/entrypoint-ferron-demo.sh:ro - ./hestia/apache-remoteip.conf:/etc/apache2/conf-available/ferron-remoteip.conf:ro environment: DEMO_USER: ${DEMO_USER:-demo} DEMO_DOMAIN: ${DEMO_DOMAIN:-demo.test} DEMO_EMAIL: ${DEMO_EMAIL:-demo@example.com} HESTIA_ADMIN_PASSWORD: ${HESTIA_ADMIN_PASSWORD:-ChangeMe123!} ports: # Panel only. Do NOT publish 80/443 from Hestia — Ferron owns them. - "${HESTIA_PANEL_PORT:-8083}:8083" networks: hosting: aliases: - hestia - apache healthcheck: # Hestia Apache binds to the container system IP (e.g. 172.30.0.2:8080), # NOT to 127.0.0.1 — probing localhost:8080 always fails and masks an # Apache outage behind the panel :8083 fallback. Probe the system IP so # `service_healthy` actually means "origin ready for Ferron". test: ["CMD-SHELL", "CUR_IP=$$(hostname -i | awk '{print $$1}'); curl -sf --max-time 5 http://$$CUR_IP:8080/ >/dev/null || exit 1"] interval: 30s timeout: 5s retries: 5 start_period: 180s restart: unless-stopped ferron: image: ${FERRON_IMAGE:-ferronserver/ferron:3-alpine} depends_on: hestia: condition: service_healthy ports: - "${FERRON_HTTP_PORT:-80}:80" - "${FERRON_HTTPS_PORT:-443}:443" - "${FERRON_METRICS_PORT:-8889}:8889" volumes: - ./ferron/ferron.conf:/etc/ferron/ferron.conf:ro - ./ferron/conf.d:/etc/ferron/conf.d:ro - ferron-dynamic:/etc/ferron/dynamic:ro - ferron-acme:/var/cache/ferron-acme - ferron-log:/var/log/ferron # TLS certs reach Ferron via the `sync` service (readable copies in # ferron-dynamic/certs + generated vhosts), NOT by mounting hestia-home # here — Hestia's ssl/ files are group-restricted and unreadable by # Ferron's `nobody` runtime user. environment: FERRON_ADMIN_TOKEN: ${FERRON_ADMIN_TOKEN:-ferron-demo-admin-token-change-me} networks: hosting: aliases: - edge healthcheck: test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:8889/metrics >/dev/null 2>&1 || wget -qO- http://127.0.0.1:80/ >/dev/null 2>&1 || exit 1"] interval: 20s timeout: 5s retries: 5 start_period: 15s restart: unless-stopped prometheus: image: prom/prometheus:v3.3.0 command: - "--config.file=/etc/prometheus/prometheus.yml" - "--storage.tsdb.path=/prometheus" - "--enable-feature=exemplar-storage" volumes: - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro - prometheus-data:/prometheus ports: - "${PROMETHEUS_PORT:-9090}:9090" networks: hosting: depends_on: ferron: condition: service_started restart: unless-stopped grafana: image: grafana/grafana:13.0 environment: GF_SECURITY_ADMIN_USER: admin GF_SECURITY_ADMIN_PASSWORD: ${GRAFANA_ADMIN_PASSWORD:-admin} GF_USERS_ALLOW_SIGN_UP: "false" volumes: - ./grafana/provisioning:/etc/grafana/provisioning:ro - grafana-data:/var/lib/grafana ports: - "${GRAFANA_PORT:-3000}:3000" networks: hosting: depends_on: prometheus: condition: service_started restart: unless-stopped sync: build: context: . dockerfile: sync/Dockerfile image: hestia-ferron-demo/sync:latest depends_on: hestia: condition: service_healthy ferron: condition: service_started volumes: - hestia-home:/hestia-home:ro - hestia-data:/hestia-data:ro - ferron-dynamic:/etc/ferron/dynamic environment: FERRON_ADMIN_TOKEN: ${FERRON_ADMIN_TOKEN:-ferron-demo-admin-token-change-me} SYNC_INTERVAL: ${SYNC_INTERVAL:-30} HESTIA_DATA: /hestia-data/data networks: hosting: restart: unless-stopped networks: hosting: name: hestia-ferron-hosting driver: bridge volumes: hestia-home: name: hestia-ferron-home hestia-data: name: hestia-ferron-hestia-data hestia-log: name: hestia-ferron-hestia-log ferron-dynamic: name: hestia-ferron-dynamic ferron-acme: name: hestia-ferron-acme ferron-log: name: hestia-ferron-log prometheus-data: name: hestia-ferron-prometheus grafana-data: name: hestia-ferron-grafana