Sets up the project skeleton: - FastAPI app factory with lifespan, request-id middleware, and RFC 9457 problem+json error handlers - Async SQLAlchemy 2.0 over MySQL (asyncmy), with a constraint naming convention in place before the first migration and async Alembic - Keycloak as a pure resource server: OIDC discovery, cached JWKS with rotation-aware refresh, and require_roles dependencies - Devcontainer running MySQL 8.4 and Keycloak 26.7 as compose siblings, with the realm (clients, roles, test users) imported on first boot - Test suite covering the endpoints plus the token validator itself, exercised against a locally generated RSA keypair - uv packaging, ruff, mypy --strict, pre-commit, Gitea CI, prod Dockerfile Two Keycloak-in-containers traps are handled explicitly and documented in the README: the issuer/internal-URL split (the browser sees localhost:8080, the API sees keycloak:8080) and the audience mapper that stops Keycloak issuing tokens with aud=account. The devices resource is a placeholder proving the routing -> auth -> ORM -> migration path end to end; replace it with the real domain. Co-Authored-By: Claude Opus 5 <[email protected]>
76 lines
2.5 KiB
YAML
76 lines
2.5 KiB
YAML
# Backing services for v2x-server.
|
|
#
|
|
# Used two ways:
|
|
# * on its own -> `docker compose up -d` for a local stack
|
|
# * with .devcontainer/compose.override.yaml -> adds the "app" dev container
|
|
#
|
|
# Credentials here are development-only and intentionally committed.
|
|
|
|
name: v2x-server
|
|
|
|
services:
|
|
mysql:
|
|
image: mysql:8.4
|
|
command:
|
|
- --character-set-server=utf8mb4
|
|
- --collation-server=utf8mb4_0900_ai_ci
|
|
environment:
|
|
MYSQL_ROOT_PASSWORD: rootpassword
|
|
MYSQL_DATABASE: v2x
|
|
MYSQL_USER: v2x
|
|
MYSQL_PASSWORD: v2xpassword
|
|
ports:
|
|
- "3306:3306"
|
|
volumes:
|
|
- mysql-data:/var/lib/mysql
|
|
# Init scripts run only on the first boot of an empty data volume.
|
|
- ./docker/mysql/init:/docker-entrypoint-initdb.d:ro
|
|
healthcheck:
|
|
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-prootpassword"]
|
|
interval: 5s
|
|
timeout: 5s
|
|
retries: 20
|
|
start_period: 30s
|
|
|
|
keycloak:
|
|
image: quay.io/keycloak/keycloak:26.7.0
|
|
command: ["start-dev", "--import-realm"]
|
|
environment:
|
|
# Keycloak 26 renamed these from KEYCLOAK_ADMIN / KEYCLOAK_ADMIN_PASSWORD.
|
|
KC_BOOTSTRAP_ADMIN_USERNAME: admin
|
|
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
|
|
# Pin the public identity so every issued token carries
|
|
# iss=http://localhost:8080/realms/v2x, regardless of which network path
|
|
# produced it. See README "The two URL traps".
|
|
KC_HOSTNAME: http://localhost:8080
|
|
KC_HOSTNAME_BACKCHANNEL_DYNAMIC: "true"
|
|
KC_HTTP_ENABLED: "true"
|
|
KC_HEALTH_ENABLED: "true"
|
|
# Dev mode keeps the realm in an embedded H2 file inside this volume --
|
|
# deliberately not MySQL, so wiping app data never destroys identity data.
|
|
KC_DB: dev-file
|
|
ports:
|
|
- "8080:8080"
|
|
- "9000:9000"
|
|
volumes:
|
|
- keycloak-data:/opt/keycloak/data
|
|
- ./docker/keycloak:/opt/keycloak/data/import:ro
|
|
healthcheck:
|
|
# The image ships no curl or wget, so probe the management port through
|
|
# bash's /dev/tcp. Informational only -- nothing blocks on it, because a
|
|
# failed probe here should not stop you from opening the devcontainer.
|
|
test:
|
|
- "CMD-SHELL"
|
|
- >-
|
|
exec 3<>/dev/tcp/127.0.0.1/9000 &&
|
|
printf 'GET /health/ready HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3 &&
|
|
cat <&3 | grep -q 'UP'
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 30
|
|
start_period: 60s
|
|
|
|
volumes:
|
|
mysql-data:
|
|
keycloak-data:
|