From b74d20cbe0ae639e60c4d39a40ff4b7e6e7dbf20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Gremaud?= Date: Sat, 19 Sep 2026 13:36:48 +0200 Subject: [PATCH] chore: deployment move to docker only --- .dockerignore | 2 +- .gitignore | 2 +- Dockerfile | 6 +- deploy/deploy.sh | 58 ---------------- deploy/docker/README.md | 24 +++---- deploy/docker/polylan-submitter.env.dist | 41 ++++++++++++ deploy/docker/supervisord.conf | 9 +-- deploy/haproxy/haproxy.cfg | 47 ------------- deploy/supervisor/polylan-submitter.conf | 66 ------------------- deploy/supervisor/polylan-submitter.env.dist | 58 ---------------- .../polylan_submitter/settings.py | 12 ++-- 11 files changed, 68 insertions(+), 257 deletions(-) delete mode 100755 deploy/deploy.sh create mode 100644 deploy/docker/polylan-submitter.env.dist delete mode 100644 deploy/haproxy/haproxy.cfg delete mode 100644 deploy/supervisor/polylan-submitter.conf delete mode 100644 deploy/supervisor/polylan-submitter.env.dist diff --git a/.dockerignore b/.dockerignore index 68ab978..5b015de 100644 --- a/.dockerignore +++ b/.dockerignore @@ -20,4 +20,4 @@ polylan_submitter/db.sqlite3 polylan_submitter/.vscode polylan_submitter/settingsLocal.py -deploy/supervisor/polylan-submitter.env +deploy/docker/polylan-submitter.env diff --git a/.gitignore b/.gitignore index a5f29f1..22384db 100644 --- a/.gitignore +++ b/.gitignore @@ -178,4 +178,4 @@ pyrightconfig.json tags media/ settingsLocal.py -deploy/supervisor/polylan-submitter.env +deploy/docker/polylan-submitter.env diff --git a/Dockerfile b/Dockerfile index 2072dd9..56c3e74 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,9 @@ # Single OCI image bundling the Vue/Vite frontend, the Django/Channels web # process (daphne) and the RQ worker, managed together by supervisord - see # deploy/docker/README.md for why (single-container Proxmox deploy) and how -# to run it. Postgres, Redis and S3 stay external, configured the same way -# as the bare-metal deploy in deploy/ (see deploy/supervisor/*.env.dist for -# the full env var list) - this image carries no infra, just the app. +# to run it. Postgres, Redis and S3 stay external (see +# deploy/docker/polylan-submitter.env.dist for the full env var list) - this +# image carries no infra, just the app. # # Build (from repo root, requires BuildKit/buildx - RUN --mount=type=cache # below is rejected by the legacy builder): diff --git a/deploy/deploy.sh b/deploy/deploy.sh deleted file mode 100755 index 6798953..0000000 --- a/deploy/deploy.sh +++ /dev/null @@ -1,58 +0,0 @@ -#!/usr/bin/env bash -# Deploys polylan-submitter from /opt/polylan-submitter: pulls latest master, -# syncs the venv, renders and installs the supervisor config, restarts the -# supervisor programs, installs the (static) HAProxy config, reloads -# HAProxy, then re-adds the backend servers over HAProxy's admin socket - -# a reload always resets HAProxy back to just the one static server in -# deploy/haproxy/haproxy.cfg, so the rest get added fresh every time. -# -# Run on the target host (needs sudo for cp/supervisorctl/systemctl/socat): -# deploy/deploy.sh - -set -euo pipefail - -APP_DIR=/opt/polylan-submitter -RUN_DIR="$APP_DIR/run" -SUPERVISOR_CONF_DIR=/etc/supervisor/conf.d -HAPROXY_CONF=/etc/haproxy/haproxy.cfg -HAPROXY_ADMIN_SOCK=/run/haproxy/admin.sock -HAPROXY_BACKEND=polylan_submitter_web - -# Single source of truth for daphne process count: drives supervisor's -# numprocs, and how many backend servers get re-added to HAProxy below -# (server 00 is already static in haproxy.cfg, so only 01..N-1 are added). -NUM_WEB_WORKERS=5 - -cd "$APP_DIR" -git pull --ff-only -uv sync --frozen - -sudo mkdir -p "$RUN_DIR" -sudo chown polylan:polylan "$RUN_DIR" - -render_dir=$(mktemp -d) -trap 'rm -rf "$render_dir"' EXIT - -sed "s/__NUM_WEB_WORKERS__/$NUM_WEB_WORKERS/" \ - "$APP_DIR/deploy/supervisor/polylan-submitter.conf" > "$render_dir/polylan-submitter.conf" -sudo cp "$render_dir/polylan-submitter.conf" "$SUPERVISOR_CONF_DIR/polylan-submitter.conf" - -sudo supervisorctl reread -sudo supervisorctl update -sudo supervisorctl restart polylan-submitter: - -sudo mkdir -p "$(dirname "$HAPROXY_ADMIN_SOCK")" -sudo cp "$APP_DIR/deploy/haproxy/haproxy.cfg" "$HAPROXY_CONF" -sudo haproxy -c -f "$HAPROXY_CONF" -sudo systemctl reload haproxy - -# The admin socket only processes one command per connection - sending -# several commands into one socat session silently drops everything after -# the first, so each command below gets its own connection. -for ((i = 1; i < NUM_WEB_WORKERS; i++)); do - printf -v socket_num "%02d" "$i" - server="${HAPROXY_BACKEND}/web-${socket_num}" - socket="unix@${RUN_DIR}/web-${socket_num}.sock" - echo "add server ${server} ${socket} check" | sudo socat - "UNIX-CONNECT:${HAPROXY_ADMIN_SOCK}" - echo "enable server ${server}" | sudo socat - "UNIX-CONNECT:${HAPROXY_ADMIN_SOCK}" -done diff --git a/deploy/docker/README.md b/deploy/docker/README.md index 174d21b..6d52e56 100644 --- a/deploy/docker/README.md +++ b/deploy/docker/README.md @@ -9,12 +9,11 @@ one container: - `worker` - two `rqworker` processes for background jobs (submission OCR, AAndT sync, etc.) -Postgres, Redis and S3-compatible storage are **not** bundled - the image -expects them the same way the bare-metal deploy does (see -`deploy/supervisor/polylan-submitter.env.dist` for the full list of env -vars: `POSTGRES_*`, `REDIS_HOST`/`REDIS_PORT`, `AWS_*`, `DJANGO_SECRET_KEY`, -etc.). Point those at wherever those services already live (other -containers/LXCs, or existing infra). +Postgres, Redis and S3-compatible storage are **not** bundled (see +`deploy/docker/polylan-submitter.env.dist` for the full list of env vars: +`POSTGRES_*`, `REDIS_HOST`/`REDIS_PORT`, `AWS_*`, `DJANGO_SECRET_KEY`, etc.). +Point those at wherever those services already live (other containers/LXCs, +or existing infra). ## Build @@ -30,7 +29,7 @@ docker buildx build --load -t polylan-submitter . ```sh docker run -d \ --name polylan-submitter \ - --env-file deploy/supervisor/polylan-submitter.env \ + --env-file deploy/docker/polylan-submitter.env \ -p 8000:8000 \ polylan-submitter ``` @@ -41,15 +40,18 @@ shared S3 bucket, so run it once per deploy instead of racing multiple replicas on every boot: ```sh -docker run --rm --env-file deploy/supervisor/polylan-submitter.env \ +docker run --rm --env-file deploy/docker/polylan-submitter.env \ polylan-submitter python manage.py collectstatic --noinput ``` ## Scaling -There's one `daphne` process per container (unlike the bare-metal deploy's -`NUM_WEB_WORKERS` behind HAProxy) - scale by running multiple container -replicas behind an external reverse proxy/load balancer instead. +There's one `daphne` process per container - it binds the only port the +container exposes, so running more of them would need a reverse proxy +inside the container too. Scale by running multiple container replicas +behind an external reverse proxy/load balancer instead (each replica also +runs its own pair of `rqworker` processes, so worker capacity scales the +same way). ## Deploying to Proxmox diff --git a/deploy/docker/polylan-submitter.env.dist b/deploy/docker/polylan-submitter.env.dist new file mode 100644 index 0000000..f95a396 --- /dev/null +++ b/deploy/docker/polylan-submitter.env.dist @@ -0,0 +1,41 @@ +# Template for the --env-file passed to `docker run` (see +# deploy/docker/README.md). Copy this to deploy/docker/polylan-submitter.env, +# fill in real values, and keep it out of git (already in .gitignore) and +# world-readable perms (chmod 600) since it holds secrets. +# +# Docker reads this file itself (KEY=VALUE per line) before the container's +# Python even starts - no python-dotenv, no $VAR/${VAR} expansion, no shell +# involved. settings.py just reads the values from os.environ. +# +# Rotating a secret means editing this file and restarting the container +# (`docker restart polylan-submitter` or replacing it, depending on how +# you're running it) - values are read fresh on every process start. +# +# See settings.py for what each of these does and its dev-mode default. + +DJANGO_SECRET_KEY= +DJANGO_DEBUG=false +DJANGO_ALLOWED_HOSTS=submitter.polylan.ch + +POSTGRES_DB= +POSTGRES_USER= +POSTGRES_PASSWORD= +POSTGRES_HOST= +POSTGRES_PORT= + +REDIS_HOST= +REDIS_PORT= +PLACE_REDIS_URL= + +AWS_ACCESS_KEY_ID= +AWS_SECRET_ACCESS_KEY= +AWS_S3_ENDPOINT_URL= +AWS_STORAGE_BUCKET_NAME= + +SENTRY_ENVIRONMENT=production +SENTRY_DSN= + +MARKET_ENABLED=true +STEAM_API_KEY= +AANDT_API_BASE_URL= +SUBMITTER_BASE_URL= diff --git a/deploy/docker/supervisord.conf b/deploy/docker/supervisord.conf index 2907e2a..5e1763d 100644 --- a/deploy/docker/supervisord.conf +++ b/deploy/docker/supervisord.conf @@ -1,10 +1,8 @@ ; Runs inside the single app container as the non-root "app" user (see ; Dockerfile's USER app - no [supervisord] user= needed, we never start as -; root here). Unlike deploy/supervisor/polylan-submitter.conf (multiple -; daphne processes behind HAProxy on one bare-metal host), this container -; runs ONE daphne process on TCP: scale by running more container replicas -; behind an external reverse proxy/load balancer instead of more processes -; per container. +; root here). This container runs ONE daphne process on TCP: scale by +; running more container replicas behind an external reverse proxy/load +; balancer instead of more processes per container. ; ; Program logs go to stdout/stderr (logfile_maxbytes=0 disables supervisor's ; own log rotation for them) so `docker logs` / Proxmox's log viewer picks @@ -29,7 +27,6 @@ stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 -; RQ workers - same command as deploy/supervisor/polylan-submitter.conf. [program:worker] command=/app/.venv/bin/python manage.py rqworker default --job-class django_tasks_rq.Job --with-scheduler directory=/app/polylan_submitter diff --git a/deploy/haproxy/haproxy.cfg b/deploy/haproxy/haproxy.cfg deleted file mode 100644 index c7d817f..0000000 --- a/deploy/haproxy/haproxy.cfg +++ /dev/null @@ -1,47 +0,0 @@ -# HAProxy config for polylan-submitter. Plain HTTP only - TLS is terminated -# upstream of this box, so nothing here handles certs or :443. -# -# Unlike nginx/Caddy's per-site snippet directories, HAProxy's default -# packaging expects one single /etc/haproxy/haproxy.cfg for the whole -# process, so this file IS the full config - deploy.sh copies it wholesale -# rather than dropping a fragment into an "enabled" dir. If this box also -# runs other things through HAProxy, fold this frontend/backend into your -# existing haproxy.cfg by hand instead of letting deploy.sh clobber it. -# -# This is no longer a template: it's a plain, ready-to-use config with one -# statically defined backend server (web-00, the first daphne socket), so -# the backend is never empty right after a reload - traffic has somewhere to -# go immediately. deploy.sh adds the remaining NUM_WEB_WORKERS-1 servers over -# the admin socket below (`add server` + `enable server`) every time it -# reloads HAProxy, since a reload always resets the running process back to -# exactly what's in this file. Deploy sequence is always: cp this file -> -# reload -> re-add the rest of the servers. -# -# WebSocket (submissions/clock/place) needs no special handling here: modern -# HAProxy in `mode http` detects the 101 Switching Protocols response and then -# just relays bytes both ways for the rest of that connection - that only -# works because keep-alive is the default connection mode below (no -# `option http-server-close` / `httpclose`, either of which would break it). -# Static files are served in-process by WhiteNoise (see MIDDLEWARE in -# settings.py), so nothing else needs to be carved out here either. - -global - log /dev/log local0 - maxconn 2000 - stats socket /run/haproxy/admin.sock mode 660 level admin - -defaults - mode http - log global - option httplog - timeout connect 5s - timeout client 60s - timeout server 60s - -frontend polylan_submitter_http - bind *:7777 - default_backend polylan_submitter_web - -backend polylan_submitter_web - balance roundrobin - server web-00 unix@/opt/polylan-submitter/run/web-00.sock check diff --git a/deploy/supervisor/polylan-submitter.conf b/deploy/supervisor/polylan-submitter.conf deleted file mode 100644 index ded8b2a..0000000 --- a/deploy/supervisor/polylan-submitter.conf +++ /dev/null @@ -1,66 +0,0 @@ -# Supervisor config template for polylan-submitter. -# -# This is a template, not a ready-to-use file: __NUM_WEB_WORKERS__ is filled -# in by deploy.sh (the same number it also uses to re-add HAProxy backend -# servers after each reload - see deploy/haproxy/haproxy.cfg), which then -# installs the rendered result into /etc/supervisor/conf.d/ and -# reread/updates supervisor. Run deploy.sh rather than copying this file by -# hand. -# -# Assumes the app is checked out at /opt/polylan-submitter with a venv at -# /opt/polylan-submitter/.venv, and a "polylan" system user owning it -# (adduser --system --group polylan). Adjust to taste. -# -# App secrets/config (DJANGO_SECRET_KEY, POSTGRES_*, AWS_*, etc. - see -# settings.py for the full list, polylan-submitter.env.dist for the file -# template) are NOT listed in the `environment=` lines below - only -# SETTINGS_ENV_FILE_LOCATION is, pointing settings.py at the file so it can -# load_dotenv() it itself. That's the only var that needs to reach the -# process from here at all: it's just a path, not a secret, so unlike the -# ~19 real secrets it's safe to write directly into this tracked file, and -# there's no %(ENV_x)s crash risk since it's a literal string, not a -# pass-through. Rotating a secret just means editing the env file and -# running `supervisorctl restart polylan-submitter:` (which deploy.sh -# already does on every deploy) - dotenv is read fresh on every process -# start, no systemd/supervisord-wide restart involved. - -[group:polylan-submitter] -programs=polylan-submitter-web,polylan-submitter-worker - -; Daphne has no built-in multi-worker/--workers flag (single Twisted reactor -; per process), so "more workers" means more daphne processes. Each one binds -; its own unix socket under run/; HAProxy load-balances across all of them -; (see deploy/haproxy/haproxy.cfg). They share state fine since channel -; layers, sessions, cache, etc. all already go through Redis/Postgres rather -; than in-process memory. __NUM_WEB_WORKERS__ comes from deploy.sh's -; NUM_WEB_WORKERS - change it there, not here. -[program:polylan-submitter-web] -command=/opt/polylan-submitter/.venv/bin/daphne -u /opt/polylan-submitter/run/web-%(process_num)02d.sock polylan_submitter.asgi:application -directory=/opt/polylan-submitter/polylan_submitter -process_name=%(program_name)s-%(process_num)02d -user=polylan -numprocs=__NUM_WEB_WORKERS__ -autostart=true -autorestart=true -stopasgroup=true -killasgroup=true -stopsignal=TERM -stdout_logfile=/var/log/supervisor/polylan-submitter-web-%(process_num)02d.log -stderr_logfile=/var/log/supervisor/polylan-submitter-web-%(process_num)02d.log -environment=DJANGO_SETTINGS_MODULE="polylan_submitter.settings",SETTINGS_ENV_FILE_LOCATION="/opt/polylan-submitter/deploy/supervisor/polylan-submitter.env" - -; RQ workers can have multiple `--with-scheduler`, there is a redis-lock -[program:polylan-submitter-worker] -command=/opt/polylan-submitter/.venv/bin/python manage.py rqworker default --job-class django_tasks_rq.Job --with-scheduler -directory=/opt/polylan-submitter/polylan_submitter -process_name=%(program_name)s-%(process_num)02d -user=polylan -numprocs=2 -autostart=true -autorestart=true -stopasgroup=true -killasgroup=true -stopsignal=TERM -stdout_logfile=/var/log/supervisor/polylan-submitter-worker-%(process_num)02d.log -stderr_logfile=/var/log/supervisor/polylan-submitter-worker-%(process_num)02d.log -environment=DJANGO_SETTINGS_MODULE="polylan_submitter.settings",SETTINGS_ENV_FILE_LOCATION="/opt/polylan-submitter/deploy/supervisor/polylan-submitter.env" diff --git a/deploy/supervisor/polylan-submitter.env.dist b/deploy/supervisor/polylan-submitter.env.dist deleted file mode 100644 index 0f457c3..0000000 --- a/deploy/supervisor/polylan-submitter.env.dist +++ /dev/null @@ -1,58 +0,0 @@ -# Template for deploy/supervisor/polylan-submitter.env inside the app -# checkout (/opt/polylan-submitter/deploy/supervisor/polylan-submitter.env on -# the target host) - that path is in .gitignore, same treatment as -# settingsLocal.py, so `git pull` never touches or overwrites it once it's -# there. -# -# Copy this file to that path, fill in real values, then: -# sudo chown polylan:polylan /opt/polylan-submitter/deploy/supervisor/polylan-submitter.env -# sudo chmod 600 /opt/polylan-submitter/deploy/supervisor/polylan-submitter.env -# (owned by "polylan", not root: this gets read by settings.py itself, which -# runs as the "polylan" user - see `user=` in polylan-submitter.conf. 600 -# still keeps every other user out.) -# -# settings.py loads this via python-dotenv (see SETTINGS_ENV_FILE_LOCATION -# in polylan-submitter.conf's `environment=`, and the load_dotenv() call -# near the top of settings.py), not a shell - there's no $VAR expansion or -# shell-special-character risk here like a bash `source` would have. The one -# thing dotenv DOES expand is ${VAR_NAME} (braced, referencing another var -# already defined above it in this same file) - if a real secret value ever -# happens to literally contain that exact pattern, quote it (single or -# double quotes both suppress dotenv's own expansion and get stripped). -# Plain values like the ones below need no quoting at all. -# -# load_dotenv() is called with override=False, so an already-exported env -# var always wins over what's in this file - it only fills gaps. It's also -# read fresh on every process start (not cached in some long-lived daemon), -# so rotating a secret just means editing this file and running -# `sudo supervisorctl restart polylan-submitter:` - which deploy.sh already -# does on every routine deploy anyway. -# -# See settings.py for what each of these does and its dev-mode default. - -DJANGO_SECRET_KEY= -DJANGO_DEBUG=false -DJANGO_ALLOWED_HOSTS=submitter.polylan.ch - -POSTGRES_DB= -POSTGRES_USER= -POSTGRES_PASSWORD= -POSTGRES_HOST= -POSTGRES_PORT= - -REDIS_HOST= -REDIS_PORT= -PLACE_REDIS_URL= - -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_S3_ENDPOINT_URL= -AWS_STORAGE_BUCKET_NAME= - -SENTRY_ENVIRONMENT=production -SENTRY_DSN= - -MARKET_ENABLED=true -STEAM_API_KEY= -AANDT_API_BASE_URL= -SUBMITTER_BASE_URL= diff --git a/polylan_submitter/polylan_submitter/settings.py b/polylan_submitter/polylan_submitter/settings.py index 4bdfac7..82388e4 100644 --- a/polylan_submitter/polylan_submitter/settings.py +++ b/polylan_submitter/polylan_submitter/settings.py @@ -19,12 +19,12 @@ from dotenv import load_dotenv BASE_DIR = Path(__file__).resolve().parent.parent # Optional: point this at a KEY=value file (see -# deploy/supervisor/polylan-submitter.env.dist for the template, and -# deploy/supervisor/polylan-submitter.conf for how it's wired up in -# production) to fill in the env vars read throughout this file. override -# =False so an already-exported env var always wins over the file - this -# only fills gaps, same as every os.environ.get() below already treating a -# merely-blank value as "use my own default" rather than "". +# deploy/docker/polylan-submitter.env.dist for the template - unused in the +# Docker deploy itself, which passes env vars directly via `docker run +# --env-file` instead) to fill in the env vars read throughout this file. +# override=False so an already-exported env var always wins over the file - +# this only fills gaps, same as every os.environ.get() below already +# treating a merely-blank value as "use my own default" rather than "". if os.environ.get("SETTINGS_ENV_FILE_LOCATION"): load_dotenv(os.environ["SETTINGS_ENV_FILE_LOCATION"], override=False)