Make release compose topology immutable and verifiable
This commit is contained in:
47
README.md
47
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`.
|
||||
|
||||
27
docker-compose.dev.yml
Normal file
27
docker-compose.dev.yml
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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"]
|
||||
|
||||
36
scripts/verify_release_topology.sh
Executable file
36
scripts/verify_release_topology.sh
Executable 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."
|
||||
Reference in New Issue
Block a user