From 3b5f1f37ddbd5247027125edb86bf5567985f165 Mon Sep 17 00:00:00 2001 From: Alfredo Di Stasio Date: Thu, 12 Mar 2026 16:40:17 +0100 Subject: [PATCH] Make release compose topology immutable and verifiable --- README.md | 47 +++++++++++++++++++++++++----- docker-compose.dev.yml | 27 +++++++++++++++++ docker-compose.release.yml | 15 ---------- docker-compose.yml | 17 ++++------- scripts/verify_release_topology.sh | 36 +++++++++++++++++++++++ 5 files changed, 108 insertions(+), 34 deletions(-) create mode 100644 docker-compose.dev.yml create mode 100755 scripts/verify_release_topology.sh diff --git a/README.md b/README.md index 8473d83..b2a3a50 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ cp .env.example .env 2. Build and run services: ```bash -docker compose up --build +docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile dev up --build ``` This starts the development-oriented topology (source bind mounts enabled). @@ -96,13 +96,22 @@ docker compose exec web python manage.py createsuperuser ## Development vs Release Compose -Development startup (mutable source, HTMX/Tailwind workflow): +Base compose (`docker-compose.yml`) is release-oriented and immutable for runtime services. +Development mutability is enabled via `docker-compose.dev.yml`. + +Development startup (mutable source bind mounts for `web`/`celery_*`): ```bash -docker compose up --build +docker compose -f docker-compose.yml -f docker-compose.dev.yml up --build ``` -Release-style startup (immutable runtime containers, no source bind mount in web/celery runtime): +Development startup with Tailwind watch: + +```bash +docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile dev up --build +``` + +Release-style startup (immutable runtime services): ```bash docker compose -f docker-compose.yml -f docker-compose.release.yml up -d --build @@ -116,11 +125,33 @@ docker compose -f docker-compose.yml -f docker-compose.release.yml down Notes: -- In release-style mode, `web`, `celery_worker`, and `celery_beat` run from the built image filesystem. -- `tailwind` is marked as `dev` profile in release override and is not started unless `--profile dev` is used. +- In release-style mode, `web`, `celery_worker`, and `celery_beat` run from built image filesystem with no repository source bind mount. +- In development mode (with `docker-compose.dev.yml`), `web`, `celery_worker`, and `celery_beat` are mutable and bind-mount `.:/app`. +- `tailwind` is a dev-profile service and is not required for release runtime. - `nginx`, `postgres`, and `redis` service naming remains unchanged. - Release-style `web`, `celery_worker`, and `celery_beat` explicitly run as container user `10001:10001`. +## Release Topology Verification + +Inspect merged release config: + +```bash +docker compose -f docker-compose.yml -f docker-compose.release.yml config +``` + +What to verify: + +- `services.web.volumes` does not include a bind mount from repository path to `/app` +- `services.celery_worker.volumes` does not include a bind mount from repository path to `/app` +- `services.celery_beat.volumes` does not include a bind mount from repository path to `/app` +- persistent named volumes still exist for `postgres_data`, `static_data`, `media_data`, `runtime_data`, and `redis_data` + +Automated local/CI-friendly check: + +```bash +./scripts/verify_release_topology.sh +``` + ## Setup and Run Notes - `web` service starts through `entrypoint.sh` and waits for PostgreSQL readiness. @@ -150,7 +181,7 @@ Notes: - `media_data`: user/provider media artifacts - `runtime_data`: app runtime files (e.g., celery beat schedule) - `redis_data`: Redis persistence (`/data` for RDB/AOF files) -- `node_modules_data`: Node modules cache for Tailwind builds in containers +- `node_modules_data`: Node modules cache for Tailwind builds in development override This keeps persistent state outside container lifecycles. @@ -207,7 +238,7 @@ sudo chown -R "$(id -u):$(id -g)" static Run Tailwind in watch mode during development: ```bash -docker compose up tailwind +docker compose -f docker-compose.yml -f docker-compose.dev.yml --profile dev up tailwind ``` Source CSS lives in `static/src/tailwind.css` and compiles to `static/css/main.css`. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..8285e76 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,27 @@ +services: + web: + user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" + volumes: + - .:/app + - node_modules_data:/app/node_modules + - static_data:/app/staticfiles + - media_data:/app/media + - runtime_data:/app/runtime + + celery_worker: + user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" + volumes: + - .:/app + - runtime_data:/app/runtime + + celery_beat: + user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" + volumes: + - .:/app + - runtime_data:/app/runtime + + tailwind: + user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" + volumes: + - .:/app + - node_modules_data:/app/node_modules diff --git a/docker-compose.release.yml b/docker-compose.release.yml index b3e573a..fba5fdc 100644 --- a/docker-compose.release.yml +++ b/docker-compose.release.yml @@ -1,30 +1,15 @@ services: web: - user: "10001:10001" - volumes: - - static_data:/app/staticfiles - - media_data:/app/media - - runtime_data:/app/runtime environment: DJANGO_SETTINGS_MODULE: config.settings.production DJANGO_DEBUG: "0" celery_worker: - user: "10001:10001" - volumes: - - runtime_data:/app/runtime environment: DJANGO_SETTINGS_MODULE: config.settings.production DJANGO_DEBUG: "0" celery_beat: - user: "10001:10001" - volumes: - - runtime_data:/app/runtime environment: DJANGO_SETTINGS_MODULE: config.settings.production DJANGO_DEBUG: "0" - - tailwind: - profiles: - - dev diff --git a/docker-compose.yml b/docker-compose.yml index 2c0e5b6..3167add 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,10 +34,8 @@ services: redis: condition: service_healthy command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers ${GUNICORN_WORKERS:-3} --access-logfile - --error-logfile - - user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" + user: "10001:10001" volumes: - - .:/app - - node_modules_data:/app/node_modules - static_data:/app/staticfiles - media_data:/app/media - runtime_data:/app/runtime @@ -58,10 +56,9 @@ services: env_file: - .env command: npm run dev - user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" - volumes: - - .:/app - - node_modules_data:/app/node_modules + user: "10001:10001" + profiles: + - dev restart: unless-stopped celery_worker: @@ -76,9 +73,8 @@ services: redis: condition: service_healthy command: celery -A config worker -l info - user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" + user: "10001:10001" volumes: - - .:/app - runtime_data:/app/runtime healthcheck: test: ["CMD-SHELL", "celery -A config inspect ping -d celery@$$HOSTNAME | grep -q pong || exit 1"] @@ -100,9 +96,8 @@ services: redis: condition: service_healthy command: celery -A config beat -l info --schedule=/app/runtime/celerybeat-schedule - user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}" + user: "10001:10001" volumes: - - .:/app - runtime_data:/app/runtime healthcheck: test: ["CMD-SHELL", "test -f /app/runtime/celerybeat-schedule || exit 1"] diff --git a/scripts/verify_release_topology.sh b/scripts/verify_release_topology.sh new file mode 100755 index 0000000..5c26490 --- /dev/null +++ b/scripts/verify_release_topology.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env sh +set -eu + +ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)" +cd "$ROOT_DIR" + +MERGED_FILE="$(mktemp)" +trap 'rm -f "$MERGED_FILE"' EXIT + +docker compose -f docker-compose.yml -f docker-compose.release.yml config > "$MERGED_FILE" + +check_service_bind_mount() { + service_name="$1" + if awk -v service=" ${service_name}:" -v root="$ROOT_DIR" ' + BEGIN { in_service = 0 } + $0 == service { in_service = 1; next } + in_service && /^ [a-zA-Z0-9_]+:/ { in_service = 0 } + in_service && /source: / { + if (index($0, root) > 0) { + print $0 + exit 1 + } + } + ' "$MERGED_FILE"; then + printf "OK: %s has no source bind mount from repository path.\n" "$service_name" + else + printf "ERROR: %s still has a source bind mount from repository path in release config.\n" "$service_name" >&2 + exit 1 + fi +} + +check_service_bind_mount "web" +check_service_bind_mount "celery_worker" +check_service_bind_mount "celery_beat" + +echo "Release topology verification passed."