deploy: consolidate Unraid stack in Dockerman
deploy / deploy (push) Canceled after 0s

This commit is contained in:
Jens
2026-07-25 23:17:01 +02:00
parent bdf44eec04
commit 8648b4f4d1
9 changed files with 214 additions and 91 deletions
+32
View File
@@ -0,0 +1,32 @@
# syntax=docker/dockerfile:1.7
FROM python:3.13-slim-trixie
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
UV_COMPILE_BYTECODE=1 \
UV_LINK_MODE=copy \
UV_DEFAULT_INDEX=https://pypi.org/simple \
PATH="/app/.venv/bin:$PATH"
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl ca-certificates libpq5 postgresql redis-server supervisor \
&& rm -rf /var/lib/apt/lists/*
COPY --from=ghcr.io/astral-sh/uv:0.10.0 /uv /uvx /bin/
WORKDIR /app
RUN addgroup --system app \
&& adduser --system --ingroup app --home /app --no-create-home app
COPY pyproject.toml uv.lock ./
RUN uv sync --frozen --no-dev --no-install-project
COPY . .
RUN uv sync --frozen --no-dev \
&& chmod +x scripts/*.sh \
&& python manage.py collectstatic --noinput \
&& mkdir -p /app/media /app/logs /app/local /var/log/supervisor \
&& chown -R app:app /app
EXPOSE 8080
ENTRYPOINT ["/app/scripts/unraid_aio_entrypoint.sh"]
+1 -1
View File
@@ -1,5 +1,5 @@
# Unraid-installatiebestanden # Unraid-installatiebestanden
De composebron staat in de repositoryroot als `docker-compose.unraid.yml`. Volg eerst [`docs/operations/UNRAID_DEPLOYMENT.md`](../../docs/operations/UNRAID_DEPLOYMENT.md). De composebron staat in de repositoryroot als `docker-compose.unraid.yml`. Deze bouwt één Dockerman-container via `Dockerfile.unraid`; interne processen worden door Supervisor bewaakt. Volg eerst [`docs/operations/UNRAID_DEPLOYMENT.md`](../../docs/operations/UNRAID_DEPLOYMENT.md).
Deze directory is gereserveerd voor toekomstige Unraid-template-XML en release-specifieke voorbeelden. Commit hier geen ingevulde `.env`, app-password, registrypulltoken of databaseback-up. Deze directory is gereserveerd voor toekomstige Unraid-template-XML en release-specifieke voorbeelden. Commit hier geen ingevulde `.env`, app-password, registrypulltoken of databaseback-up.
+58
View File
@@ -0,0 +1,58 @@
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
pidfile=/tmp/supervisord.pid
[program:postgres]
command=/usr/lib/postgresql/17/bin/postgres -D /app/local/postgres-aio17
user=postgres
priority=10
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:redis]
command=/usr/bin/redis-server --dir /app/local/redis-aio --save 60 1 --loglevel warning --protected-mode yes --bind 127.0.0.1
user=redis
priority=20
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:web]
command=/app/.venv/bin/gunicorn config.wsgi:application --bind 0.0.0.0:8080 --workers 2 --threads 4 --timeout 90 --no-control-socket
directory=/app
user=app
priority=40
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:worker]
command=/app/.venv/bin/celery -A config worker -l INFO -Q high,default,low --concurrency=2
directory=/app
user=app
priority=50
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:scheduler]
command=/app/.venv/bin/celery -A config beat -l INFO --schedule /tmp/celerybeat-schedule
directory=/app
user=app
priority=60
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
+31 -69
View File
@@ -1,76 +1,38 @@
# Compose-bron van waarheid met paden voor een Unraid-installatie. # Eén Dockerman-container met intern bewaakte PostgreSQL-, Redis-, web- en Celery-processen.
# Lees deployment/unraid/README.md vóór gebruik en wijzig secrets.
name: vacatureradar name: vacatureradar
x-app: &app
image: ghcr.io/CHANGE_ME/vacatureradar:latest
user: "10001:10001"
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid,size=128m
- /var/tmp:rw,noexec,nosuid,size=128m
- /app/.cache:rw,noexec,nosuid,size=128m
env_file:
- /mnt/user/appdata/vacatureradar/config/.env
restart: unless-stopped
security_opt:
- no-new-privileges:true
volumes:
- /mnt/user/appdata/vacatureradar/media:/app/media
- /mnt/user/appdata/vacatureradar/logs:/app/logs
- /mnt/user/appdata/vacatureradar/local:/app/local
networks: [internal]
depends_on:
postgres: {condition: service_healthy}
redis: {condition: service_healthy}
services: services:
web: app:
<<: *app build:
ports: ["1226:8080"] context: .
worker: dockerfile: Dockerfile.unraid
<<: *app image: vacatureradar:unraid
container_name: VacatureRadar
env_file:
- /mnt/user/appdata/vacatureradar/source/.env
environment: environment:
VACATURERADAR_RUN_MIGRATIONS: "0" DATABASE_URL: postgresql://vacatureradar:${POSTGRES_PASSWORD}@127.0.0.1:5432/vacatureradar
cap_drop: POSTGRES_HOST: 127.0.0.1
- ALL REDIS_URL: redis://127.0.0.1:6379/0
command: ["celery", "-A", "config", "worker", "-l", "INFO", "-Q", "high,default,low", "--concurrency=2"] VACATURERADAR_RESTORE_DUMP: /app/local/backups/aio-migration.dump
scheduler:
<<: *app
environment:
VACATURERADAR_RUN_MIGRATIONS: "0"
cap_drop:
- ALL
command: ["celery", "-A", "config", "beat", "-l", "INFO", "--schedule", "/tmp/celerybeat-schedule"]
postgres:
image: postgres:17.5-bookworm
restart: unless-stopped restart: unless-stopped
env_file: [/mnt/user/appdata/vacatureradar/config/.env] security_opt:
volumes: [/mnt/user/appdata/vacatureradar/postgres:/var/lib/postgresql/data] - no-new-privileges:true
networks: [internal] ports:
- "1226:8080"
volumes:
- /mnt/user/appdata/vacatureradar/source/local/media:/app/media
- /mnt/user/appdata/vacatureradar/source/local/logs:/app/logs
- /mnt/user/appdata/vacatureradar/source/local:/app/local
labels:
net.unraid.docker.managed: dockerman
net.unraid.docker.webui: http://[IP]:[PORT:8080]/
net.unraid.docker.icon: http://[IP]:[PORT:8080]/static/favicon.svg
org.opencontainers.image.title: VacatureRadar
org.opencontainers.image.description: Persoonlijke vacature-intelligence voor de Belgische arbeidsmarkt
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U vacatureradar -d vacatureradar"] test: ["CMD", "python", "scripts/healthcheck.py", "http://127.0.0.1:8080/health/ready/"]
interval: 10s interval: 30s
timeout: 5s timeout: 5s
retries: 10 retries: 5
redis: start_period: 60s
image: redis:7.4.2-alpine
restart: unless-stopped
command: ["redis-server", "--save", "60", "1", "--loglevel", "warning"]
volumes: [/mnt/user/appdata/vacatureradar/redis:/data]
networks: [internal]
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 10
ollama:
image: ollama/ollama:0.9.6
profiles: ["ai"]
restart: unless-stopped
volumes: [/mnt/user/appdata/vacatureradar/ollama:/root/.ollama]
networks: [internal]
networks:
internal:
driver: bridge
+3 -2
View File
@@ -1,6 +1,6 @@
# Projectstatus # Projectstatus
- Laatst bijgewerkt: 2026-07-22 - Laatst bijgewerkt: 2026-07-25
- Repositoryversie: 0.3.0 volledige Stitch Intelligence Cockpit - Repositoryversie: 0.3.0 volledige Stitch Intelligence Cockpit
- Uitvoeringsmodus: autonome backlog - Uitvoeringsmodus: autonome backlog
- Eerste uitvoerbare taak: controleer met `python scripts/backlog.py next` - Eerste uitvoerbare taak: controleer met `python scripts/backlog.py next`
@@ -184,7 +184,8 @@ VR-208-herverificatie op 2026-07-22:
## Deploymentstatus ## Deploymentstatus
- `main` is via de bestaande Gitea-SSH-sleutel naar `NuklearRabbit/VacatureRadar` gepusht. - `main` is via de bestaande Gitea-SSH-sleutel naar `NuklearRabbit/VacatureRadar` gepusht.
- De applicatie draait op de Unraid-server via `http://192.168.10.150:1226/` met PostgreSQL, Redis, web, worker en scheduler. - De applicatie draait op de Unraid-server via `http://192.168.10.150:1226/` als één Dockerman-container; Supervisor bewaakt intern PostgreSQL 17, Redis, web, worker en scheduler.
- De Dockerman-tegel gebruikt de lokale VacatureRadar-favicon en opent rechtstreeks de WebUI op poort 1226.
- De server-side `.env` heeft rechten `0600`; gegenereerde secrets zijn niet naar Git of logs gekopieerd. - De server-side `.env` heeft rechten `0600`; gegenereerde secrets zijn niet naar Git of logs gekopieerd.
- De self-hosted Gitea Actions-runner is niet actief. Deze sessie gebruikte daarom een directe, commitgebonden SSH-deploy; push-to-deploy vereist nog runnerherstel/registratie. - De self-hosted Gitea Actions-runner is niet actief. Deze sessie gebruikte daarom een directe, commitgebonden SSH-deploy; push-to-deploy vereist nog runnerherstel/registratie.
- De huidige uitrol is alleen geschikt voor het beheerde LAN. Publieke productie vereist eerst een gekozen domein, reverse proxy/TLS en `DJANGO_DEBUG=0` met secure cookies. - De huidige uitrol is alleen geschikt voor het beheerde LAN. Publieke productie vereist eerst een gekozen domein, reverse proxy/TLS en `DJANGO_DEBUG=0` met secure cookies.
+16 -10
View File
@@ -1,6 +1,6 @@
# Unraid-deployment # Unraid-deployment
Deze handleiding gebruikt `docker-compose.unraid.yml` als declaratieve bron van waarheid. Unraid-installaties verschillen in gebruikte Compose Manager, reverse proxy en shares; behoud de hieronder genoemde volumes, secrets en netwerkgrenzen ook wanneer de UI de services afzonderlijk aanmaakt. Deze handleiding gebruikt `docker-compose.unraid.yml` als declaratieve bron van waarheid. De Unraid-variant bundelt web, worker, scheduler, PostgreSQL en Redis bewust in één Dockerman-container met Supervisor als procesbewaker. De gewone `docker-compose.yml` behoudt de gescheiden productieprocessen voor andere hosts.
## Vooraf ## Vooraf
@@ -10,17 +10,25 @@ Maak deze directories:
/mnt/user/appdata/vacatureradar/config /mnt/user/appdata/vacatureradar/config
/mnt/user/appdata/vacatureradar/media /mnt/user/appdata/vacatureradar/media
/mnt/user/appdata/vacatureradar/logs /mnt/user/appdata/vacatureradar/logs
/mnt/user/appdata/vacatureradar/postgres /mnt/user/appdata/vacatureradar/source/local/postgres-aio17
/mnt/user/appdata/vacatureradar/redis /mnt/user/appdata/vacatureradar/source/local/redis-aio
/mnt/user/appdata/vacatureradar/ollama # alleen bij lokale AI /mnt/user/appdata/vacatureradar/ollama # alleen bij lokale AI
/mnt/user/backups/vacatureradar /mnt/user/backups/vacatureradar
``` ```
Gebruik bij voorkeur een cache-backed `appdata`-share en neem die afzonderlijk in je back-upregime op. De PostgreSQL-datadir mag niet gelijktijdig door file-syncsoftware worden gemuteerd. Gebruik bij voorkeur een cache-backed `appdata`-share en neem die afzonderlijk in je back-upregime op. De PostgreSQL-datadir mag niet gelijktijdig door file-syncsoftware worden gemuteerd.
## Dockerman
De container verschijnt als **VacatureRadar** en bevat metadata voor:
- WebUI: `http://[IP]:1226/`;
- icoon: het lokale VacatureRadar-favicon op `/static/favicon.svg`;
- één herstart- en healthcheckpunt voor alle interne processen.
## Imagekeuze ## Imagekeuze
De Unraid-compose bevat bewust `ghcr.io/CHANGE_ME/vacatureradar:latest`. Codex kan code, Dockerfile en CI afwerken zonder registrycredentials, maar voor productie moet een van deze paden worden gekozen: De Unraid-compose bouwt lokaal met `Dockerfile.unraid` en tagt de image als `vacatureradar:unraid`. Pin voor reproduceerbare releases aanvullend een commitgebonden tag.
1. bouw lokaal op de Unraid-host en vervang `image:` door een vaste lokale tag; 1. bouw lokaal op de Unraid-host en vervang `image:` door een vaste lokale tag;
2. publiceer vanuit CI naar een private registry en pin op een immutable digest; 2. publiceer vanuit CI naar een private registry en pin op een immutable digest;
@@ -61,9 +69,7 @@ SESSION_COOKIE_SECURE=1 CSRF_COOKIE_SECURE=1 SECURE_SSL_REDIRECT=1 \
bash scripts/deploy_docker.sh bash scripts/deploy_docker.sh
``` ```
3. Start eerst PostgreSQL en Redis. 3. Start de ene appcontainer; de entrypoint initialiseert PostgreSQL, voert migraties uit en start daarna alle processen onder Supervisor.
4. Start web; de entrypoint voert migraties uit.
5. Start worker en scheduler.
6. Controleer `GET /health/live/` en `GET /health/ready/`. 6. Controleer `GET /health/live/` en `GET /health/ready/`.
6. Voer eenmalig in de webcontainer uit: 6. Voer eenmalig in de webcontainer uit:
@@ -144,10 +150,10 @@ Pin een modelnaam in `OLLAMA_MODEL`; zonder model blijft `OLLAMA_ENABLED=0`. Ste
2. Lees `CHANGELOG.md` en de releaseartefacten. 2. Lees `CHANGELOG.md` en de releaseartefacten.
3. Als de release nog niet geverifieerd is, voer `./scripts/release_verify.sh` lokaal uit. 3. Als de release nog niet geverifieerd is, voer `./scripts/release_verify.sh` lokaal uit.
4. Pull/bouw de nieuwe immutable image. 4. Pull/bouw de nieuwe immutable image.
5. Stop scheduler en worker, daarna web. 5. Stop de appcontainer.
6. Start web en laat migraties uitvoeren. 6. Start de nieuwe appcontainer en laat de entrypoint migraties uitvoeren.
7. Controleer health, login en release-smoke-rapport. 7. Controleer health, login en release-smoke-rapport.
8. Start worker en scheduler. 8. Controleer in de containerlogs dat web, worker en scheduler actief zijn.
9. Bewaar de vorige image/digest totdat één volledige schedulercyclus goed verliep. 9. Bewaar de vorige image/digest totdat één volledige schedulercyclus goed verliep.
## Rollback ## Rollback
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
set -euo pipefail
pg_data=/app/local/postgres-aio17
redis_data=/app/local/redis-aio
pg_bin="$(dirname "$(readlink -f "$(command -v pg_config)")")"
fresh_database=0
mkdir -p "$pg_data" "$redis_data" /app/media /app/logs /tmp/celery
chown -R postgres:postgres "$pg_data"
chown -R redis:redis "$redis_data"
chown -R app:app /app/media /app/logs /tmp/celery
if [[ ! -s "$pg_data/PG_VERSION" ]]; then
runuser -u postgres -- "$pg_bin/initdb" -D "$pg_data" --auth-local=trust --auth-host=scram-sha-256
fresh_database=1
fi
runuser -u postgres -- "$pg_bin/pg_ctl" -D "$pg_data" -o "-c listen_addresses=127.0.0.1" -w start
db_name="${POSTGRES_DB:-vacatureradar}"
db_user="${POSTGRES_USER:-vacatureradar}"
db_password="${POSTGRES_PASSWORD:?POSTGRES_PASSWORD ontbreekt}"
export DATABASE_URL="postgresql://${db_user}:${db_password}@127.0.0.1:5432/${db_name}"
export POSTGRES_HOST=127.0.0.1
export REDIS_URL=redis://127.0.0.1:6379/0
runuser -u postgres -- psql --set=ON_ERROR_STOP=1 --set=db_name="$db_name" --set=db_user="$db_user" --set=db_password="$db_password" postgres <<'SQL'
SELECT format('CREATE ROLE %I LOGIN PASSWORD %L', :'db_user', :'db_password')
WHERE NOT EXISTS (SELECT FROM pg_roles WHERE rolname = :'db_user') \gexec
SELECT format('ALTER ROLE %I PASSWORD %L', :'db_user', :'db_password') \gexec
SELECT format('CREATE DATABASE %I OWNER %I', :'db_name', :'db_user')
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = :'db_name') \gexec
SQL
if [[ "$fresh_database" == "1" && -n "${VACATURERADAR_RESTORE_DUMP:-}" && -s "${VACATURERADAR_RESTORE_DUMP}" ]]; then
runuser -u postgres -- pg_restore --no-owner --role="$db_user" --dbname="$db_name" "${VACATURERADAR_RESTORE_DUMP}"
fi
runuser -u postgres -- "$pg_bin/pg_ctl" -D "$pg_data" -m fast -w stop
runuser -u postgres -- "$pg_bin/pg_ctl" -D "$pg_data" -o "-c listen_addresses=127.0.0.1" -w start
runuser -u app -- python manage.py migrate --noinput
if [[ "${VACATURERADAR_AUTO_BOOTSTRAP:-0}" == "1" ]]; then
runuser -u app -- python manage.py bootstrap_instance || true
fi
runuser -u postgres -- "$pg_bin/pg_ctl" -D "$pg_data" -m fast -w stop
exec /usr/bin/supervisord -c /app/deployment/unraid/supervisord.conf
+6 -1
View File
@@ -83,7 +83,12 @@ def validate_yaml_contracts() -> None:
compose = parse_yaml(relative) compose = parse_yaml(relative)
if not isinstance(compose, dict) or not isinstance(compose.get("services"), dict): if not isinstance(compose, dict) or not isinstance(compose.get("services"), dict):
raise ValidationError(f"Composebestand mist services: {relative}") raise ValidationError(f"Composebestand mist services: {relative}")
for service in ("web", "worker", "scheduler", "postgres", "redis"): required_services = (
("app",)
if relative == "docker-compose.unraid.yml"
else ("web", "worker", "scheduler", "postgres", "redis")
)
for service in required_services:
if service not in compose["services"]: if service not in compose["services"]:
raise ValidationError(f"{relative} mist service {service}") raise ValidationError(f"{relative} mist service {service}")
+21 -8
View File
@@ -7,15 +7,28 @@ ROOT = Path(__file__).resolve().parents[2]
def test_only_web_runs_migrations_in_compose() -> None: def test_only_web_runs_migrations_in_compose() -> None:
for filename in ("docker-compose.yml", "docker-compose.unraid.yml"): compose = yaml.safe_load((ROOT / "docker-compose.yml").read_text(encoding="utf-8"))
compose = yaml.safe_load((ROOT / filename).read_text(encoding="utf-8")) services = compose["services"]
services = compose["services"]
assert ( assert services["web"].get("environment", {}).get("VACATURERADAR_RUN_MIGRATIONS", "1") == "1"
services["web"].get("environment", {}).get("VACATURERADAR_RUN_MIGRATIONS", "1") == "1" assert services["worker"]["environment"]["VACATURERADAR_RUN_MIGRATIONS"] == "0"
) assert services["scheduler"]["environment"]["VACATURERADAR_RUN_MIGRATIONS"] == "0"
assert services["worker"]["environment"]["VACATURERADAR_RUN_MIGRATIONS"] == "0"
assert services["scheduler"]["environment"]["VACATURERADAR_RUN_MIGRATIONS"] == "0"
def test_unraid_uses_one_dockerman_container_with_ui_metadata() -> None:
compose = yaml.safe_load((ROOT / "docker-compose.unraid.yml").read_text(encoding="utf-8"))
assert list(compose["services"]) == ["app"]
labels = compose["services"]["app"]["labels"]
assert labels["net.unraid.docker.managed"] == "dockerman"
assert labels["net.unraid.docker.webui"].endswith(":8080]/")
assert labels["net.unraid.docker.icon"].endswith("/static/favicon.svg")
dockerfile = (ROOT / "Dockerfile.unraid").read_text(encoding="utf-8")
entrypoint = (ROOT / "scripts" / "unraid_aio_entrypoint.sh").read_text(encoding="utf-8")
assert "python:3.13-slim-trixie" in dockerfile
assert "postgres-aio17" in entrypoint
assert "command -v pg_config" in entrypoint
def test_runtime_is_compatible_with_read_only_container() -> None: def test_runtime_is_compatible_with_read_only_container() -> None: