############################################################################### # Production stack. # # Four containers and one database. Deliberately no Redis: Postgres holds the # cache, the sessions and the queue, which for a site this size is plenty and # is one less thing to run, back up and forget to secure. Swapping it in later # is three environment variables. # # docker compose --env-file .env.production up -d --build # # The web container is the only one that migrates, so a deploy runs them once # rather than once per replica. ############################################################################### x-app: &app build: context: . dockerfile: Dockerfile image: pireon-admin:latest restart: unless-stopped env_file: .env.production environment: DB_CONNECTION: pgsql DB_HOST: postgres DB_PORT: 5432 # The maintenance flag and the health badge are read on nearly every # request, so the cache has to be shared by every container — a per-process # one would have the panel and the API disagreeing about whether the site # is down. CACHE_STORE: database SESSION_DRIVER: database QUEUE_CONNECTION: database volumes: # Uploaded images live here, and they are the one thing in this stack that # cannot be rebuilt from the repository. - storage:/app/storage depends_on: postgres: condition: service_healthy services: app: <<: *app environment: DB_CONNECTION: pgsql DB_HOST: postgres DB_PORT: 5432 CACHE_STORE: database SESSION_DRIVER: database QUEUE_CONNECTION: database # Only here: migrations run once per deploy, not once per container. RUN_MIGRATIONS: "true" ports: - "${APP_PORT:-8000}:80" healthcheck: test: ["CMD", "curl", "-fsS", "http://localhost/up"] interval: 30s timeout: 5s start_period: 40s retries: 3 # Image processing and the inbox notifications. Without this, uploads serve # without dimensions or srcset and the panel's bell stays silent — the site # keeps working, which is exactly why the absence is easy to miss. worker: <<: *app command: ["php", "artisan", "queue:work", "--tries=3", "--max-time=3600", "--sleep=3"] # Waits for the web container, not just the database: it is the one that # migrates, and a worker that starts against an empty schema spends its # first minute crash-looping on tables that do not exist yet. depends_on: postgres: condition: service_healthy app: condition: service_healthy healthcheck: test: ["CMD", "php", "artisan", "queue:monitor", "default"] interval: 60s timeout: 10s start_period: 40s retries: 3 stop_grace_period: 30s # Closes expired calls, sweeps up images the queue missed and refreshes the # git repositories. `schedule:work` is the long-running form of a crontab. scheduler: <<: *app command: ["php", "artisan", "schedule:work"] # The FrankenPHP base image ships a healthcheck that probes Caddy's admin # API, and this container does not serve HTTP — inherited, it could never # pass. Replaced rather than disabled, because `compose up --wait` refuses # a service with no healthcheck at all. # # What it proves is modest and stated plainly: the application still boots # and can read its schedule. It does not prove the loop is running; if that # dies, the process dies with it and `restart: unless-stopped` brings it # back. healthcheck: test: ["CMD", "php", "artisan", "schedule:list"] interval: 60s timeout: 15s start_period: 40s retries: 3 depends_on: postgres: condition: service_healthy app: condition: service_healthy stop_grace_period: 30s postgres: image: postgres:17-alpine restart: unless-stopped environment: POSTGRES_DB: ${DB_DATABASE:-pireon} POSTGRES_USER: ${DB_USERNAME:-pireon} POSTGRES_PASSWORD: ${DB_PASSWORD:?set DB_PASSWORD in .env.production} # Content is written by a handful of people and read constantly; the # default collation rules are what the JSON slug lookups sort under. POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=C" volumes: - pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME:-pireon} -d ${DB_DATABASE:-pireon}"] interval: 10s timeout: 5s retries: 5 start_period: 20s # Not published: nothing outside this stack has any business reaching the # database. Use `docker compose exec postgres psql` to get in. expose: - "5432" volumes: pgdata: storage: