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
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.
+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.
# Lees deployment/unraid/README.md vóór gebruik en wijzig secrets.
# Eén Dockerman-container met intern bewaakte PostgreSQL-, Redis-, web- en Celery-processen.
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:
web:
<<: *app
ports: ["1226:8080"]
worker:
<<: *app
app:
build:
context: .
dockerfile: Dockerfile.unraid
image: vacatureradar:unraid
container_name: VacatureRadar
env_file:
- /mnt/user/appdata/vacatureradar/source/.env
environment:
VACATURERADAR_RUN_MIGRATIONS: "0"
cap_drop:
- ALL
command: ["celery", "-A", "config", "worker", "-l", "INFO", "-Q", "high,default,low", "--concurrency=2"]
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
DATABASE_URL: postgresql://vacatureradar:${POSTGRES_PASSWORD}@127.0.0.1:5432/vacatureradar
POSTGRES_HOST: 127.0.0.1
REDIS_URL: redis://127.0.0.1:6379/0
VACATURERADAR_RESTORE_DUMP: /app/local/backups/aio-migration.dump
restart: unless-stopped
env_file: [/mnt/user/appdata/vacatureradar/config/.env]
volumes: [/mnt/user/appdata/vacatureradar/postgres:/var/lib/postgresql/data]
networks: [internal]
security_opt:
- no-new-privileges:true
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:
test: ["CMD-SHELL", "pg_isready -U vacatureradar -d vacatureradar"]
interval: 10s
test: ["CMD", "python", "scripts/healthcheck.py", "http://127.0.0.1:8080/health/ready/"]
interval: 30s
timeout: 5s
retries: 10
redis:
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
retries: 5
start_period: 60s
+3 -2
View File
@@ -1,6 +1,6 @@
# Projectstatus
- Laatst bijgewerkt: 2026-07-22
- Laatst bijgewerkt: 2026-07-25
- Repositoryversie: 0.3.0 volledige Stitch Intelligence Cockpit
- Uitvoeringsmodus: autonome backlog
- Eerste uitvoerbare taak: controleer met `python scripts/backlog.py next`
@@ -184,7 +184,8 @@ VR-208-herverificatie op 2026-07-22:
## Deploymentstatus
- `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 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.
+16 -10
View File
@@ -1,6 +1,6 @@
# 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
@@ -10,17 +10,25 @@ Maak deze directories:
/mnt/user/appdata/vacatureradar/config
/mnt/user/appdata/vacatureradar/media
/mnt/user/appdata/vacatureradar/logs
/mnt/user/appdata/vacatureradar/postgres
/mnt/user/appdata/vacatureradar/redis
/mnt/user/appdata/vacatureradar/source/local/postgres-aio17
/mnt/user/appdata/vacatureradar/source/local/redis-aio
/mnt/user/appdata/vacatureradar/ollama # alleen bij lokale AI
/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.
## 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
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;
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
```
3. Start eerst PostgreSQL en Redis.
4. Start web; de entrypoint voert migraties uit.
5. Start worker en scheduler.
3. Start de ene appcontainer; de entrypoint initialiseert PostgreSQL, voert migraties uit en start daarna alle processen onder Supervisor.
6. Controleer `GET /health/live/` en `GET /health/ready/`.
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.
3. Als de release nog niet geverifieerd is, voer `./scripts/release_verify.sh` lokaal uit.
4. Pull/bouw de nieuwe immutable image.
5. Stop scheduler en worker, daarna web.
6. Start web en laat migraties uitvoeren.
5. Stop de appcontainer.
6. Start de nieuwe appcontainer en laat de entrypoint migraties uitvoeren.
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.
## 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)
if not isinstance(compose, dict) or not isinstance(compose.get("services"), dict):
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"]:
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:
for filename in ("docker-compose.yml", "docker-compose.unraid.yml"):
compose = yaml.safe_load((ROOT / filename).read_text(encoding="utf-8"))
services = compose["services"]
compose = yaml.safe_load((ROOT / "docker-compose.yml").read_text(encoding="utf-8"))
services = compose["services"]
assert (
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["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"
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: