# Clusev — PROD stack. Plain Docker Compose, no Portainer. # # Caddy is the ONLY host-exposed service (auto-TLS when APP_DOMAIN is set; plain # HTTP on the bare IP otherwise). app/reverb are internal-only on the clusev net. # # Bootstrap + operate with ./install.sh. Manual deploy: # docker compose -f docker-compose.prod.yml build # docker compose -f docker-compose.prod.yml up -d # docker compose -f docker-compose.prod.yml exec -u app app php artisan migrate --force name: clusev # Locally-built tag by default; override CLUSEV_IMAGE with a GHCR digest # (ghcr.io//clusev@sha256:...) once CI signs + publishes images. x-image: &image image: "${CLUSEV_IMAGE:-clusev-app:prod}" restart: unless-stopped env_file: - .env networks: - clusev services: app: <<: *image build: context: . dockerfile: Dockerfile target: prod expose: - "80" depends_on: mariadb: condition: service_healthy redis: condition: service_started reverb: <<: *image command: php artisan reverb:start --host=0.0.0.0 --port=8080 expose: - "8080" depends_on: redis: condition: service_started queue: <<: *image command: php artisan queue:work --tries=3 --timeout=90 --sleep=1 depends_on: mariadb: condition: service_healthy redis: condition: service_started caddy: image: caddy:2-alpine # pin by @sha256 digest before the first prod push restart: unless-stopped ports: - "${APP_PORT:-80}:80" - "443:443" - "443:443/udp" # HTTP/3 environment: # Domain is dashboard-driven via on-demand TLS (Caddy asks app:/_caddy/ask), so # no SITE_ADDRESS is needed. Only the ACME contact comes from .env. ACME_EMAIL: "${ACME_EMAIL:-admin@localhost}" volumes: - ./docker/caddy/Caddyfile:/etc/caddy/Caddyfile:ro - caddy-data:/data # ACME account + certs — MUST persist across recreate - caddy-config:/config depends_on: - app - reverb networks: - clusev mariadb: image: mariadb:11 restart: unless-stopped environment: MARIADB_DATABASE: "${DB_DATABASE:-clusev}" MARIADB_USER: "${DB_USERNAME:-clusev}" MARIADB_PASSWORD: "${DB_PASSWORD:?set DB_PASSWORD in .env}" MARIADB_ROOT_PASSWORD: "${DB_ROOT_PASSWORD:?set DB_ROOT_PASSWORD in .env}" volumes: - mariadb-data:/var/lib/mysql healthcheck: test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 10s timeout: 5s retries: 30 networks: - clusev # no host ports in prod — DB is reachable only on the internal clusev network redis: image: redis:7-alpine restart: unless-stopped command: redis-server --appendonly yes --requirepass "${REDIS_PASSWORD:?set REDIS_PASSWORD in .env}" volumes: - redis-data:/data networks: - clusev volumes: mariadb-data: redis-data: caddy-data: caddy-config: networks: clusev: driver: bridge