Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer collective, backed by the Pireon OAuth identity server. The public site is a separate, server-rendered application consuming /api/public/v1. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
144 lines
4.6 KiB
YAML
144 lines
4.6 KiB
YAML
###############################################################################
|
|
# 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:
|