Make release compose topology immutable and verifiable

This commit is contained in:
Alfredo Di Stasio
2026-03-12 16:40:17 +01:00
parent dac63f9148
commit 3b5f1f37dd
5 changed files with 108 additions and 34 deletions

View File

@ -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`.

27
docker-compose.dev.yml Normal file
View File

@ -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

View File

@ -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

View File

@ -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"]

View File

@ -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."