diff --git a/Dockerfile b/Dockerfile index 1f7ae3a..e7c1d2a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,29 +1,32 @@ -FROM python:3.12.2-alpine +FROM python:3.12-slim-bookworm ARG TARGETARCH ARG TARGETVARIANT -RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apk/repositories - ENV OJ_ENV=production WORKDIR /app COPY ./deploy/requirements.txt /app/deploy/ -RUN --mount=type=cache,target=/var/cache/apk,id=apk-cache-$TARGETARCH$TARGETVARIANT-final \ +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-cache-$TARGETARCH$TARGETVARIANT-final \ --mount=type=cache,target=/root/.cache/pip,id=pip-cache-$TARGETARCH$TARGETVARIANT-final \ < "$DATA/config/secret.key" + echo "$(head -c 32 /dev/urandom | md5sum | head -c 32)" > "$DATA/config/secret.key" fi if [ ! -f "$DATA/public/avatar/default.png" ]; then - cp data/public/avatar/default.png $DATA/public/avatar + cp data/public/avatar/default.png "$DATA/public/avatar" fi if [ ! -f "$DATA/public/website/favicon.ico" ]; then - cp data/public/website/favicon.ico $DATA/public/website + cp data/public/website/favicon.ico "$DATA/public/website" fi SSL="$DATA/ssl" if [ ! -f "$SSL/server.key" ]; then openssl req -x509 -newkey rsa:2048 -keyout "$SSL/server.key" -out "$SSL/server.crt" -days 1000 \ - -subj "/C=CN/ST=Beijing/L=Beijing/O=Beijing OnlineJudge Technology Co., Ltd./OU=Service Infrastructure Department/CN=`hostname`" -nodes + -subj "/C=CN/ST=Beijing/L=Beijing/O=Beijing OnlineJudge Technology Co., Ltd./OU=Service Infrastructure Department/CN=$(hostname)" -nodes fi -cd $APP/deploy/nginx +cd "$APP/deploy/nginx" ln -sf locations.conf https_locations.conf if [ -z "$FORCE_HTTPS" ]; then ln -sf locations.conf http_locations.conf @@ -31,29 +31,30 @@ else ln -sf https_redirect.conf http_locations.conf fi -if [ ! -z "$LOWER_IP_HEADER" ]; then +if [ -n "$LOWER_IP_HEADER" ]; then sed -i "s/__IP_HEADER__/\$http_$LOWER_IP_HEADER/g" api_proxy.conf; else sed -i "s/__IP_HEADER__/\$remote_addr/g" api_proxy.conf; fi if [ -z "$MAX_WORKER_NUM" ]; then - export CPU_CORE_NUM=$(grep -c ^processor /proc/cpuinfo) - if [[ $CPU_CORE_NUM -lt 2 ]]; then + CPU_CORE_NUM=$(grep -c ^processor /proc/cpuinfo) + export CPU_CORE_NUM + if [ "$CPU_CORE_NUM" -lt 2 ]; then export MAX_WORKER_NUM=2 else - export MAX_WORKER_NUM=$(($CPU_CORE_NUM)) + export MAX_WORKER_NUM="$CPU_CORE_NUM" fi fi -cd $APP/dist -if [ ! -z "$STATIC_CDN_HOST" ]; then +cd "$APP/dist" +if [ -n "$STATIC_CDN_HOST" ]; then find . -name "*.*" -type f -exec sed -i "s/__STATIC_CDN_HOST__/\/$STATIC_CDN_HOST/g" {} \; else find . -name "*.*" -type f -exec sed -i "s/__STATIC_CDN_HOST__\///g" {} \; fi -cd $APP +cd "$APP" n=0 while [ $n -lt 5 ] @@ -63,15 +64,19 @@ do echo "from options.options import SysOptions; SysOptions.judge_server_token='$JUDGE_SERVER_TOKEN'" | python manage.py shell && echo "from conf.models import JudgeServer; JudgeServer.objects.update(task_number=0)" | python manage.py shell && break - n=$(($n+1)) + n=$((n + 1)) echo "Failed to migrate, going to retry..." sleep 8 done -addgroup -g 903 spj -adduser -u 900 -S -G spj server +if ! getent group spj >/dev/null; then + groupadd --system --gid 903 spj +fi +if ! id -u server >/dev/null 2>&1; then + useradd --system --uid 900 --gid spj --no-create-home --shell /usr/sbin/nologin server +fi -chown -R server:spj $DATA $APP/dist -find $DATA/test_case -type d -exec chmod 710 {} \; -find $DATA/test_case -type f -exec chmod 640 {} \; +chown -R server:spj "$DATA" "$APP/dist" +find "$DATA/test_case" -type d -exec chmod 710 {} \; +find "$DATA/test_case" -type f -exec chmod 640 {} \; exec supervisord -c /app/deploy/supervisord.conf diff --git a/deploy/nginx/nginx.conf b/deploy/nginx/nginx.conf index 6165ad1..4beafdc 100644 --- a/deploy/nginx/nginx.conf +++ b/deploy/nginx/nginx.conf @@ -1,4 +1,4 @@ -user nginx; +user www-data; daemon off; pid /tmp/nginx.pid; worker_processes auto; @@ -63,4 +63,3 @@ add_header X-XSS-Protection "1; mode=block" always; # } } - diff --git a/docs/superpowers/plans/2026-06-14-debian-slim-image.md b/docs/superpowers/plans/2026-06-14-debian-slim-image.md new file mode 100644 index 0000000..b883cae --- /dev/null +++ b/docs/superpowers/plans/2026-06-14-debian-slim-image.md @@ -0,0 +1,226 @@ +# Debian Slim Backend Image Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Replace the Alpine backend image with Debian slim while improving dependency installation speed and preserving runtime behavior. + +**Architecture:** Keep the existing single-stage backend image and process model. Use Debian runtime packages plus manylinux Python wheels, then adapt the shell entrypoint and nginx worker account to Debian conventions. + +**Tech Stack:** Docker BuildKit, Python 3.12 slim-bookworm, Debian apt, POSIX shell, nginx, supervisord + +--- + +### Task 1: Establish Migration Checks + +**Files:** +- Inspect: `Dockerfile` +- Inspect: `deploy/entrypoint.sh` +- Inspect: `deploy/nginx/nginx.conf` + +- [x] **Step 1: Verify the current base image check fails** + +Run: + +```bash +grep -q '^FROM python:3\.12-slim-bookworm$' Dockerfile +``` + +Expected: exit status 1 because the current image is Alpine. + +- [x] **Step 2: Verify the current shell portability check fails** + +Run: + +```bash +sh -n deploy/entrypoint.sh +! grep -n '\[\[' deploy/entrypoint.sh +``` + +Expected: `sh -n` succeeds, but the grep assertion exits non-zero because the script contains `[[`. + +- [x] **Step 3: Verify the current nginx user check fails** + +Run: + +```bash +grep -q '^user www-data;$' deploy/nginx/nginx.conf +``` + +Expected: exit status 1 because the configuration uses the Alpine `nginx` account. + +### Task 2: Migrate the Docker Build + +**Files:** +- Modify: `Dockerfile` + +- [x] **Step 1: Replace the Alpine build with Debian slim** + +Use this package and cache structure: + +```dockerfile +FROM python:3.12-slim-bookworm +ARG TARGETARCH +ARG TARGETVARIANT + +ENV OJ_ENV=production +WORKDIR /app + +COPY ./deploy/requirements.txt /app/deploy/ + +RUN --mount=type=cache,target=/var/cache/apt,sharing=locked,id=apt-cache-$TARGETARCH$TARGETVARIANT-final \ + --mount=type=cache,target=/root/.cache/pip,id=pip-cache-$TARGETARCH$TARGETVARIANT-final \ + </dev/null; then + groupadd --system --gid 903 spj +fi +if ! id -u server >/dev/null 2>&1; then + useradd --system --uid 900 --gid spj --no-create-home --shell /usr/sbin/nologin server +fi +``` + +Quote filesystem paths and variable expansions touched by the script. + +- [x] **Step 2: Change the nginx worker user** + +Set the first line of `deploy/nginx/nginx.conf` to: + +```nginx +user www-data; +``` + +- [x] **Step 3: Run runtime configuration checks** + +Run: + +```bash +sh -n deploy/entrypoint.sh +! grep -n '\[\[' deploy/entrypoint.sh +! grep -nE 'addgroup|adduser' deploy/entrypoint.sh +grep -q 'groupadd --system --gid 903 spj' deploy/entrypoint.sh +grep -q 'useradd --system --uid 900 --gid spj' deploy/entrypoint.sh +grep -q '^user www-data;$' deploy/nginx/nginx.conf +``` + +Expected: all checks exit 0. + +### Task 4: Build And Inspect The Image + +**Files:** +- Verify: `Dockerfile` +- Verify: `deploy/entrypoint.sh` +- Verify: `deploy/nginx/nginx.conf` + +- [ ] **Step 1: Build the backend image** + +Run: + +```bash +DOCKER_BUILDKIT=1 docker build --progress=plain -t onlinejudge-backend:slim . +``` + +Expected: exit status 0, with Python dependencies installed from wheels. + +- [ ] **Step 2: Verify native dependencies and runtime tools** + +Run: + +```bash +docker run --rm --entrypoint /bin/sh onlinejudge-backend:slim -c \ + 'python -c "from PIL import Image; import lxml.etree; import psycopg" && + clang-format --version && + nginx -t -c /app/deploy/nginx/nginx.conf && + supervisord --version && + openssl version && + groupadd --help >/dev/null && + useradd --help >/dev/null' +``` + +Expected: exit status 0. + +- [ ] **Step 3: Exercise distribution-specific entrypoint startup** + +Run the image with unreachable local service names and a timeout: + +```bash +timeout 15 docker run --rm \ + -e POSTGRES_HOST=127.0.0.1 \ + -e REDIS_HOST=127.0.0.1 \ + -e JUDGE_SERVER_TOKEN=test \ + onlinejudge-backend:slim +``` + +Expected: the application may time out while retrying database migration, but output must not contain shell syntax errors, missing `groupadd`/`useradd`, missing shared libraries, or an unknown nginx user. + +### Task 5: Final Verification + +**Files:** +- Verify: all modified files + +- [ ] **Step 1: Run repository checks** + +Run: + +```bash +git diff --check +sh -n deploy/entrypoint.sh +git status --short +git diff -- Dockerfile deploy/entrypoint.sh deploy/nginx/nginx.conf +``` + +Expected: no whitespace errors or shell syntax errors; the diff contains only the approved migration and its documentation. + +- [ ] **Step 2: Record build evidence** + +Record the image ID and size: + +```bash +docker image inspect onlinejudge-backend:slim --format '{{.Id}} {{.Size}}' +``` + +Expected: one image ID and byte size. diff --git a/docs/superpowers/specs/2026-06-14-debian-slim-image-design.md b/docs/superpowers/specs/2026-06-14-debian-slim-image-design.md index 92918ac..0355c9e 100644 --- a/docs/superpowers/specs/2026-06-14-debian-slim-image-design.md +++ b/docs/superpowers/specs/2026-06-14-debian-slim-image-design.md @@ -25,6 +25,7 @@ Replace Alpine `apk` commands with Debian `apt-get` commands. Install only runti - `libpq5` - `nginx` - `openssl` +- `passwd` - `supervisor` - `unzip` - `zlib1g`