diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..68ab978 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,23 @@ +.git +.gitignore +.venv +venv +**/__pycache__ +**/*.pyc +*.egg-info +.ruff_cache +.pytest_cache +ctagsfile +images +main.py +gifs_opus_magnum + +polylan_submitter/node_modules +polylan_submitter/static +polylan_submitter/static_source/vite +polylan_submitter/media +polylan_submitter/db.sqlite3 +polylan_submitter/.vscode +polylan_submitter/settingsLocal.py + +deploy/supervisor/polylan-submitter.env diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2072dd9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,75 @@ +# 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. +# +# Build (from repo root, requires BuildKit/buildx - RUN --mount=type=cache +# below is rejected by the legacy builder): +# docker buildx build --load -t polylan-submitter . + +# ---- frontend build ------------------------------------------------------- +FROM node:22-alpine AS frontend-builder +WORKDIR /app +COPY polylan_submitter/package.json polylan_submitter/pnpm-lock.yaml ./ +RUN corepack enable && corepack prepare pnpm@9.0.0 --activate \ + && pnpm install --frozen-lockfile +COPY polylan_submitter/vite.config.ts polylan_submitter/tsconfig*.json ./ +COPY polylan_submitter/src ./src +COPY polylan_submitter/public ./public +RUN pnpm build + +# ---- python dependencies --------------------------------------------------- +FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim AS python-builder +ENV UV_COMPILE_BYTECODE=1 UV_LINK_MODE=copy +WORKDIR /app +COPY pyproject.toml uv.lock ./ +# --no-install-project: this pyproject has no [build-system]/package to +# install, it only pins the app's dependencies - so just build the venv. +RUN --mount=type=cache,target=/root/.cache/uv \ + uv sync --frozen --no-dev --no-install-project + +# ---- runtime ---------------------------------------------------------------- +FROM python:3.13-slim AS runtime + +# tesseract-ocr: shelled out to by pytesseract (submissions/tasks.py, +# opus_magnum/utils.py) for OCR'ing Opus Magnum submission GIFs. +# libgl1/libglib2.0-0: runtime shared libs opencv-python needs even though +# it ships as a wheel. +# libpq5: psycopg (plain, not psycopg[binary]/[c]) loads libpq via ctypes. +# supervisor/tini: process supervision - see deploy/docker/supervisord.conf. +RUN apt-get update && apt-get install -y --no-install-recommends \ + tesseract-ocr \ + libgl1 \ + libglib2.0-0 \ + libpq5 \ + supervisor \ + tini \ + && rm -rf /var/lib/apt/lists/* + +RUN groupadd --system app && useradd --system --gid app --create-home app + +WORKDIR /app +COPY --from=python-builder --chown=app:app /app/.venv /app/.venv +ENV PATH="/app/.venv/bin:$PATH" \ + PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + DJANGO_SETTINGS_MODULE=polylan_submitter.settings + +COPY --chown=app:app polylan_submitter/ /app/polylan_submitter/ +COPY --from=frontend-builder --chown=app:app /app/static_source/vite /app/polylan_submitter/static_source/vite +COPY --chown=app:app deploy/docker/entrypoint.sh /app/entrypoint.sh +COPY --chown=app:app deploy/docker/supervisord.conf /app/supervisord.conf + +RUN rm -rf /app/polylan_submitter/node_modules /app/polylan_submitter/settingsLocal.py \ + && mkdir -p /app/polylan_submitter/static /app/polylan_submitter/media \ + && chown app:app /app/polylan_submitter/static /app/polylan_submitter/media \ + && chmod +x /app/entrypoint.sh + +USER app +WORKDIR /app/polylan_submitter +EXPOSE 8000 + +ENTRYPOINT ["tini", "--", "/app/entrypoint.sh"] +CMD ["supervisord", "-c", "/app/supervisord.conf", "-n"] diff --git a/deploy/docker/README.md b/deploy/docker/README.md new file mode 100644 index 0000000..174d21b --- /dev/null +++ b/deploy/docker/README.md @@ -0,0 +1,71 @@ +# Container image + +`Dockerfile` (repo root) builds a single OCI image with the Vite frontend +already built in, and a `.venv` with all Python deps. At runtime it uses +`supervisord` (`deploy/docker/supervisord.conf`) to run two processes in the +one container: + +- `web` - one `daphne` process serving the Django/Channels app on `:8000` +- `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). + +## Build + +Requires BuildKit/buildx (`RUN --mount=type=cache` in the Dockerfile is +rejected by the legacy builder): + +```sh +docker buildx build --load -t polylan-submitter . +``` + +## Run + +```sh +docker run -d \ + --name polylan-submitter \ + --env-file deploy/supervisor/polylan-submitter.env \ + -p 8000:8000 \ + polylan-submitter +``` + +Migrations run automatically on container start (set `MIGRATE_ON_START=false` +to skip). `collectstatic` does not run automatically - it pushes to the +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 \ + 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. + +## Deploying to Proxmox + +Recent Proxmox VE versions can create an LXC container directly from an OCI +image (`pct create ...` with an OCI-format image +reference, currently a tech-preview feature - check `pct create --help` and +the Proxmox VE docs for your installed version, since the exact flags have +been changing across releases). Practically that means: + +1. Push the built image to a registry Proxmox can reach (a local registry, + GHCR, Docker Hub, etc.) - `docker tag`/`docker push`. +2. Create the container on the Proxmox host from that image reference, + passing the same env vars as above. +3. Map/forward port 8000 as needed, and put whatever TLS-terminating + reverse proxy you already run in front of it. + +If OCI-to-LXC isn't available on your Proxmox version, the same image runs +fine in a plain VM (or an LXC) with any container runtime (Docker/Podman) +installed instead. diff --git a/deploy/docker/entrypoint.sh b/deploy/docker/entrypoint.sh new file mode 100644 index 0000000..5cec323 --- /dev/null +++ b/deploy/docker/entrypoint.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# Container entrypoint: runs migrations (unless disabled), then execs +# whatever CMD supplied (supervisord by default - see the Dockerfile). +# +# collectstatic is intentionally NOT run here: it pushes to the shared S3 +# bucket (see STORAGES in settings.py), so running it on every container +# start would race across replicas for no benefit - run it once per deploy +# instead: +# docker run --rm --env-file polylan-submitter \ +# python manage.py collectstatic --noinput + +set -euo pipefail + +if [ "${MIGRATE_ON_START:-true}" = "true" ]; then + echo "Running migrations..." + python manage.py migrate --noinput +fi + +exec "$@" diff --git a/deploy/docker/push.sh b/deploy/docker/push.sh new file mode 100755 index 0000000..aa6fdb3 --- /dev/null +++ b/deploy/docker/push.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env sh +# One-off build+push to the Gitea container registry for testing. +# Usage: deploy/docker/push.sh [tag] (run from repo root, default tag: test) +set -eu + +REGISTRY=git.legrems.mom +OWNER=polylan +IMAGE=polylan-submitter +TAG="${1:-test}" + +cd "$(dirname "$0")/../.." + +REF="$REGISTRY/$OWNER/$IMAGE:$TAG" +# buildx (BuildKit) is required: the Dockerfile uses RUN --mount=type=cache +# for the uv dep cache, which the legacy builder rejects. +docker buildx build --load -t "$REF" . +docker push "$REF" + +echo "Pushed $REF" diff --git a/deploy/docker/supervisord.conf b/deploy/docker/supervisord.conf new file mode 100644 index 0000000..2907e2a --- /dev/null +++ b/deploy/docker/supervisord.conf @@ -0,0 +1,46 @@ +; 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. +; +; 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 +; them up - no log files inside the container to manage. + +[supervisord] +nodaemon=true +pidfile=/tmp/supervisord.pid +logfile=/dev/null +logfile_maxbytes=0 + +[program:web] +command=/app/.venv/bin/daphne -b 0.0.0.0 -p 8000 polylan_submitter.asgi:application +directory=/app/polylan_submitter +autostart=true +autorestart=true +stopasgroup=true +killasgroup=true +stopsignal=TERM +stdout_logfile=/dev/stdout +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 +process_name=%(program_name)s-%(process_num)02d +numprocs=2 +autostart=true +autorestart=true +stopasgroup=true +killasgroup=true +stopsignal=TERM +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0