chore: deployment move to docker only

This commit is contained in:
2026-09-19 13:36:48 +02:00
parent 59b568ff41
commit b74d20cbe0
11 changed files with 68 additions and 257 deletions
+1 -1
View File
@@ -20,4 +20,4 @@ polylan_submitter/db.sqlite3
polylan_submitter/.vscode polylan_submitter/.vscode
polylan_submitter/settingsLocal.py polylan_submitter/settingsLocal.py
deploy/supervisor/polylan-submitter.env deploy/docker/polylan-submitter.env
+1 -1
View File
@@ -178,4 +178,4 @@ pyrightconfig.json
tags tags
media/ media/
settingsLocal.py settingsLocal.py
deploy/supervisor/polylan-submitter.env deploy/docker/polylan-submitter.env
+3 -3
View File
@@ -1,9 +1,9 @@
# Single OCI image bundling the Vue/Vite frontend, the Django/Channels web # Single OCI image bundling the Vue/Vite frontend, the Django/Channels web
# process (daphne) and the RQ worker, managed together by supervisord - see # process (daphne) and the RQ worker, managed together by supervisord - see
# deploy/docker/README.md for why (single-container Proxmox deploy) and how # 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 # to run it. Postgres, Redis and S3 stay external (see
# as the bare-metal deploy in deploy/ (see deploy/supervisor/*.env.dist for # deploy/docker/polylan-submitter.env.dist for the full env var list) - this
# the full env var list) - this image carries no infra, just the app. # image carries no infra, just the app.
# #
# Build (from repo root, requires BuildKit/buildx - RUN --mount=type=cache # Build (from repo root, requires BuildKit/buildx - RUN --mount=type=cache
# below is rejected by the legacy builder): # below is rejected by the legacy builder):
-58
View File
@@ -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
+13 -11
View File
@@ -9,12 +9,11 @@ one container:
- `worker` - two `rqworker` processes for background jobs (submission OCR, - `worker` - two `rqworker` processes for background jobs (submission OCR,
AAndT sync, etc.) AAndT sync, etc.)
Postgres, Redis and S3-compatible storage are **not** bundled - the image Postgres, Redis and S3-compatible storage are **not** bundled (see
expects them the same way the bare-metal deploy does (see `deploy/docker/polylan-submitter.env.dist` for the full list of env vars:
`deploy/supervisor/polylan-submitter.env.dist` for the full list of env `POSTGRES_*`, `REDIS_HOST`/`REDIS_PORT`, `AWS_*`, `DJANGO_SECRET_KEY`, etc.).
vars: `POSTGRES_*`, `REDIS_HOST`/`REDIS_PORT`, `AWS_*`, `DJANGO_SECRET_KEY`, Point those at wherever those services already live (other containers/LXCs,
etc.). Point those at wherever those services already live (other or existing infra).
containers/LXCs, or existing infra).
## Build ## Build
@@ -30,7 +29,7 @@ docker buildx build --load -t polylan-submitter .
```sh ```sh
docker run -d \ docker run -d \
--name polylan-submitter \ --name polylan-submitter \
--env-file deploy/supervisor/polylan-submitter.env \ --env-file deploy/docker/polylan-submitter.env \
-p 8000:8000 \ -p 8000:8000 \
polylan-submitter polylan-submitter
``` ```
@@ -41,15 +40,18 @@ shared S3 bucket, so run it once per deploy instead of racing multiple
replicas on every boot: replicas on every boot:
```sh ```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 polylan-submitter python manage.py collectstatic --noinput
``` ```
## Scaling ## Scaling
There's one `daphne` process per container (unlike the bare-metal deploy's There's one `daphne` process per container - it binds the only port the
`NUM_WEB_WORKERS` behind HAProxy) - scale by running multiple container container exposes, so running more of them would need a reverse proxy
replicas behind an external reverse proxy/load balancer instead. 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 ## Deploying to Proxmox
+41
View File
@@ -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=
+3 -6
View File
@@ -1,10 +1,8 @@
; Runs inside the single app container as the non-root "app" user (see ; 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 ; Dockerfile's USER app - no [supervisord] user= needed, we never start as
; root here). Unlike deploy/supervisor/polylan-submitter.conf (multiple ; root here). This container runs ONE daphne process on TCP: scale by
; daphne processes behind HAProxy on one bare-metal host), this container ; running more container replicas behind an external reverse proxy/load
; runs ONE daphne process on TCP: scale by running more container replicas ; balancer instead of more processes per container.
; 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 ; 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 ; 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=/dev/stderr
stderr_logfile_maxbytes=0 stderr_logfile_maxbytes=0
; RQ workers - same command as deploy/supervisor/polylan-submitter.conf.
[program:worker] [program:worker]
command=/app/.venv/bin/python manage.py rqworker default --job-class django_tasks_rq.Job --with-scheduler command=/app/.venv/bin/python manage.py rqworker default --job-class django_tasks_rq.Job --with-scheduler
directory=/app/polylan_submitter directory=/app/polylan_submitter
-47
View File
@@ -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
-66
View File
@@ -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"
@@ -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=
@@ -19,12 +19,12 @@ from dotenv import load_dotenv
BASE_DIR = Path(__file__).resolve().parent.parent BASE_DIR = Path(__file__).resolve().parent.parent
# Optional: point this at a KEY=value file (see # Optional: point this at a KEY=value file (see
# deploy/supervisor/polylan-submitter.env.dist for the template, and # deploy/docker/polylan-submitter.env.dist for the template - unused in the
# deploy/supervisor/polylan-submitter.conf for how it's wired up in # Docker deploy itself, which passes env vars directly via `docker run
# production) to fill in the env vars read throughout this file. override # --env-file` instead) to fill in the env vars read throughout this file.
# =False so an already-exported env var always wins over the file - this # override=False so an already-exported env var always wins over the file -
# only fills gaps, same as every os.environ.get() below already treating a # this only fills gaps, same as every os.environ.get() below already
# merely-blank value as "use my own default" rather than "". # treating a merely-blank value as "use my own default" rather than "".
if os.environ.get("SETTINGS_ENV_FILE_LOCATION"): if os.environ.get("SETTINGS_ENV_FILE_LOCATION"):
load_dotenv(os.environ["SETTINGS_ENV_FILE_LOCATION"], override=False) load_dotenv(os.environ["SETTINGS_ENV_FILE_LOCATION"], override=False)