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:
|
2. Build and run services:
|
||||||
|
|
||||||
```bash
|
```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).
|
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 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
|
```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
|
```bash
|
||||||
docker compose -f docker-compose.yml -f docker-compose.release.yml up -d --build
|
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:
|
Notes:
|
||||||
|
|
||||||
- In release-style mode, `web`, `celery_worker`, and `celery_beat` run from the built image filesystem.
|
- In release-style mode, `web`, `celery_worker`, and `celery_beat` run from built image filesystem with no repository source bind mount.
|
||||||
- `tailwind` is marked as `dev` profile in release override and is not started unless `--profile dev` is used.
|
- 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.
|
- `nginx`, `postgres`, and `redis` service naming remains unchanged.
|
||||||
- Release-style `web`, `celery_worker`, and `celery_beat` explicitly run as container user `10001:10001`.
|
- 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
|
## Setup and Run Notes
|
||||||
|
|
||||||
- `web` service starts through `entrypoint.sh` and waits for PostgreSQL readiness.
|
- `web` service starts through `entrypoint.sh` and waits for PostgreSQL readiness.
|
||||||
@ -150,7 +181,7 @@ Notes:
|
|||||||
- `media_data`: user/provider media artifacts
|
- `media_data`: user/provider media artifacts
|
||||||
- `runtime_data`: app runtime files (e.g., celery beat schedule)
|
- `runtime_data`: app runtime files (e.g., celery beat schedule)
|
||||||
- `redis_data`: Redis persistence (`/data` for RDB/AOF files)
|
- `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.
|
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:
|
Run Tailwind in watch mode during development:
|
||||||
|
|
||||||
```bash
|
```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`.
|
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:
|
services:
|
||||||
web:
|
web:
|
||||||
user: "10001:10001"
|
|
||||||
volumes:
|
|
||||||
- static_data:/app/staticfiles
|
|
||||||
- media_data:/app/media
|
|
||||||
- runtime_data:/app/runtime
|
|
||||||
environment:
|
environment:
|
||||||
DJANGO_SETTINGS_MODULE: config.settings.production
|
DJANGO_SETTINGS_MODULE: config.settings.production
|
||||||
DJANGO_DEBUG: "0"
|
DJANGO_DEBUG: "0"
|
||||||
|
|
||||||
celery_worker:
|
celery_worker:
|
||||||
user: "10001:10001"
|
|
||||||
volumes:
|
|
||||||
- runtime_data:/app/runtime
|
|
||||||
environment:
|
environment:
|
||||||
DJANGO_SETTINGS_MODULE: config.settings.production
|
DJANGO_SETTINGS_MODULE: config.settings.production
|
||||||
DJANGO_DEBUG: "0"
|
DJANGO_DEBUG: "0"
|
||||||
|
|
||||||
celery_beat:
|
celery_beat:
|
||||||
user: "10001:10001"
|
|
||||||
volumes:
|
|
||||||
- runtime_data:/app/runtime
|
|
||||||
environment:
|
environment:
|
||||||
DJANGO_SETTINGS_MODULE: config.settings.production
|
DJANGO_SETTINGS_MODULE: config.settings.production
|
||||||
DJANGO_DEBUG: "0"
|
DJANGO_DEBUG: "0"
|
||||||
|
|
||||||
tailwind:
|
|
||||||
profiles:
|
|
||||||
- dev
|
|
||||||
|
|||||||
@ -34,10 +34,8 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
command: gunicorn config.wsgi:application --bind 0.0.0.0:8000 --workers ${GUNICORN_WORKERS:-3} --access-logfile - --error-logfile -
|
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:
|
volumes:
|
||||||
- .:/app
|
|
||||||
- node_modules_data:/app/node_modules
|
|
||||||
- static_data:/app/staticfiles
|
- static_data:/app/staticfiles
|
||||||
- media_data:/app/media
|
- media_data:/app/media
|
||||||
- runtime_data:/app/runtime
|
- runtime_data:/app/runtime
|
||||||
@ -58,10 +56,9 @@ services:
|
|||||||
env_file:
|
env_file:
|
||||||
- .env
|
- .env
|
||||||
command: npm run dev
|
command: npm run dev
|
||||||
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
|
user: "10001:10001"
|
||||||
volumes:
|
profiles:
|
||||||
- .:/app
|
- dev
|
||||||
- node_modules_data:/app/node_modules
|
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
celery_worker:
|
celery_worker:
|
||||||
@ -76,9 +73,8 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
command: celery -A config worker -l info
|
command: celery -A config worker -l info
|
||||||
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
|
user: "10001:10001"
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
|
||||||
- runtime_data:/app/runtime
|
- runtime_data:/app/runtime
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "celery -A config inspect ping -d celery@$$HOSTNAME | grep -q pong || exit 1"]
|
test: ["CMD-SHELL", "celery -A config inspect ping -d celery@$$HOSTNAME | grep -q pong || exit 1"]
|
||||||
@ -100,9 +96,8 @@ services:
|
|||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
command: celery -A config beat -l info --schedule=/app/runtime/celerybeat-schedule
|
command: celery -A config beat -l info --schedule=/app/runtime/celerybeat-schedule
|
||||||
user: "${LOCAL_UID:-1000}:${LOCAL_GID:-1000}"
|
user: "10001:10001"
|
||||||
volumes:
|
volumes:
|
||||||
- .:/app
|
|
||||||
- runtime_data:/app/runtime
|
- runtime_data:/app/runtime
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "test -f /app/runtime/celerybeat-schedule || exit 1"]
|
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