This commit is contained in:
@@ -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,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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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}")
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user