Initial commit: Pireon Admin — headless admin panel and public API

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>
This commit is contained in:
2026-08-19 17:55:27 +02:00
commit 3897113313
534 changed files with 64050 additions and 0 deletions

143
docker-compose.yml Normal file
View File

@@ -0,0 +1,143 @@
###############################################################################
# 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: