*_build.tar.gz) from the SecBoard license server to a clean Linux host.
Protected code ships as Cython or Nuitka — match Python 3.12
(version, SOABI, patch level where relevant) and glibc to your artifact.
The authoritative runbook is DEPLOYMENT_INSTRUCTIONS.md in your delivery package (hardware through
rollback and troubleshooting). This page is a condensed companion. After the server is up, use
PROJECT_SETUP_GUIDE.md for application setup (license, first login, admin checks).
Goal & audience
Mid-level Linux / DevOps engineers: extract the build once, configure .env, run
check --deploy, migrate, and collectstatic, run Gunicorn on
SecBoard.wsgi:application on loopback behind nginx with TLS,
and — where your edition includes them — Celery worker and Celery beat with
Redis, all from the same venv, paths, and .env as Gunicorn.
Hardware (indicative)
| Resource | Minimum (light load) | Production (typical) |
|---|---|---|
| CPU | 2 vCPU | 4+ vCPU, tune to load |
| RAM | 4 GB | 8+ GB; separate DB host preferred |
| Disk | 20 GB free | 40+ GB; plan for logs and media/ |
| Network | Outbound HTTPS for pip | DNS to this host for TLS (e.g. Let's Encrypt) |
Software & network
- OS: e.g. Ubuntu Server LTS; align glibc with the build host (see gate P8).
- Python 3.12 +
python3.12-venv; nginx; DB client libs perrequirements.txt. - Database created and reachable; Redis if Celery is used.
- Artifact:
*_build.tar.gzwith SHA256 on the change ticket.
sudo apt-get update
sudo apt-get install -y python3.12 python3.12-venv python3.12-dev nginx curl ca-certificates build-essential
# Add libpq-dev OR default-libmysqlclient-dev per requirements.txt
Pre-flight (gates)
| # | Check |
|---|---|
| P1 | python3.12 --version matches the build / release notes. |
| P1b | python3.12 -c "import sysconfig; print(sysconfig.get_config_var('SOABI'))" (e.g. cpython-312-x86_64-linux-gnu) — record on change ticket. |
| P1c | python3.12 -c "import sys; print(sys.version)" — full banner for support. |
| P2 | Know Cython vs Nuitka for your artifact. |
| P3 | Archive path and SHA256 on the change ticket. |
| P4 | Database reachable; credentials ready for .env. |
| P5 | Sudo or deploy role: install path, /etc/systemd/system/, nginx (and Supervisor only if that is your standard). |
| P6 | Outbound HTTPS for pip install -r requirements.txt (or an internal mirror). |
| P7 | OS packages as above; DB client libraries per requirements.txt. |
| P8 | ldd --version | head -n1 — compare glibc with the build host; older distros may need a matching artifact. |
| P9 | Install root exists and is empty before extract, e.g. sudo mkdir -p /srv/SecBoard_yoursite. |
| P10 | (If Celery) Redis installed/reachable; REDIS_* in .env; redis.service active before worker and beat. |
Architecture (brief)
Client → nginx :443 → Gunicorn (127.0.0.1:PORT) → Django → DB / Redis
↑
Celery worker / beat (if present)
Gunicorn listens on loopback only; expose 80/443 via nginx.
Placeholders
| Placeholder | Meaning |
|---|---|
/srv/SecBoard_yoursite | Install root (empty on greenfield). |
yoursite | Public DNS hostname. |
secboard_yoursite.service | Your systemd unit name for Gunicorn. |
youruser:yourgroup | OS user/group for Gunicorn/Celery (venv and manage.py must use the same user). |
rm -rf over data you need. If you replace an existing install later, back up the database,
media/, and configs first; prefer mv the old directory aside (see DEPLOYMENT_INSTRUCTIONS.md — Operations, security, rollback).
End-to-end sequence (condensed)
0 prerequisites → 1 stop units (no-op until first install) → 2–3 inspect tar, extract, chown →
4 venv + pip → 5 .env + check --deploy → 6 dirs, migrate, collectstatic, chmod →
7 optional .so smoke test → 8–11 Gunicorn, systemd ReadWritePaths, nginx, start → 12 TLS after DNS →
13 verification → 14 optional firewall (allow SSH first). Use exactly one process manager for the app:
systemd or Supervisor, not both for the same process.
Inspect archive, then extract
Run tar -tzf your_build.tar.gz | head once. The inner top-level folder name often differs from the .tar.gz basename.
If there is exactly one top-level directory, use --strip-components=1; if files sit at archive root, omit it.
sudo systemctl stop secboard_yoursite.service 2>/dev/null || true
# If Celery uses separate units, stop them here too.
sudo mkdir -p /srv/SecBoard_yoursite
tar -tzf /path/to/yoursite_build.tar.gz | head
sudo tar -xzf /path/to/yoursite_build.tar.gz -C /srv/SecBoard_yoursite --strip-components=1
sudo chown -R youruser:yourgroup /srv/SecBoard_yoursite
Python ABI gate, venv, Django
cd /srv/SecBoard_yoursite
python3.12 -c "import sys; print(sys.version)"
python3.12 -c "import sysconfig; print(sysconfig.get_config_var('SOABI'))"
ldd --version | head -n1
python3.12 -m venv venv
source venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install -r requirements.txt
cp -n .env.example .env
# Edit .env: DB_*, SECRET_KEY, ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS
# (include http://127.0.0.1:<gunicorn_port> if admins SSH port-forward to the real --bind port),
# CORS_*, REDIS_*, PUBLIC_BASE_URL, SITE_*, DEBUG=0. Do NOT keep a bundled .env from the build machine.
python3.12 manage.py check --deploy
mkdir -p logs media staticfiles
python3.12 manage.py showmigrations
python3.12 manage.py migrate --noinput
python3.12 manage.py collectstatic --noinput
chmod -R 755 staticfiles media
chmod -R 775 logs
After any .env change: restart Gunicorn and (if used) Celery worker and beat —
sudo systemctl restart …. Workers read the environment at process start; stale values can cause
DisallowedHost and a misleading HTTP 400 with CSRF/session text (see troubleshooting in DEPLOYMENT_INSTRUCTIONS.md).
Optional: compiled build smoke test
cd /srv/SecBoard_yoursite && source venv/bin/activate
ls -la app_conf/*.so 2>/dev/null || ls -la app_conf/
python3.12 -c "from app_conf import models; print('OK:', models.__file__)"
Celery worker, Redis, and Celery beat (editions that include them)
When your edition ships background tasks: Redis running before worker/beat; REDIS_* in .env must match.
Prefer separate systemd units (or separate Supervisor programs) for Gunicorn, worker, and beat.
Same identity as Gunicorn: User= / Group= / WorkingDirectory=, venv PATH, DJANGO_SETTINGS_MODULE=SecBoard.settings (or as shipped in deploy/).
With ProtectSystem=strict, include the install directory in ReadWritePaths= along with logs, media, and staticfiles.
Example dependencies: After=network.target redis.service, Wants=redis.service. Many builds use
django-celery-beat with --scheduler=django_celery_beat.schedulers:DatabaseScheduler — exact ExecStart is in
DEPLOYMENT_INSTRUCTIONS.md and any deploy/*.service samples. After every new tar deploy, restart Celery so code matches Gunicorn.
Gunicorn (WSGI)
Use only SecBoard.wsgi:application (matches SecBoard/wsgi.py; a Nuitka bootstrap .py is normal).
Put --bind 127.0.0.1:PORT in systemd or Supervisor; nginx proxy_pass must target that loopback port.
gunicorn SecBoard.wsgi:application \
--bind 127.0.0.1:8000 \
--workers 3 \
--timeout 60 \
--max-requests 1000 \
--max-requests-jitter 100
nginx & TLS
Terminate TLS at nginx; forward Host, X-Real-IP, X-Forwarded-For, and X-Forwarded-Proto to Gunicorn.
After edits: sudo nginx -t then reload. Obtain certificates only when DNS points to this host (e.g. Certbot).
Post-deploy checks (important)
When Django uses SECURE_SSL_REDIRECT, a naive curl http://127.0.0.1:PORT/… may return 301 to https://127.0.0.1:PORT,
which then fails because Gunicorn on that port speaks plain HTTP, not TLS. Prefer probing the app through nginx on 443 from the same host:
curl -fsS --resolve "yoursite:443:127.0.0.1" "https://yoursite/" || exit 1
systemctl is-active secboard_yoursite.service 2>/dev/null || true
tail -n 80 /srv/SecBoard_yoursite/logs/gunicorn_error.log
Replace yoursite and paths with your deployment. Use curl -f / --fail in automation so 4xx/5xx are errors.
Optional firewall
If using ufw, allow SSH (22/tcp) before ufw enable so you are not locked out. Example:
sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
License validation: The SecBoard platform (the host where the product is deployed) must have outbound HTTPS access to https://license.secboard.online/ for license verification, activation, and heartbeats when your edition uses the central license service. If you restrict egress (host firewall, cloud security group, or corporate proxy), allow that endpoint (and any other license URLs in your product docs); otherwise those checks may fail.
Rollback (high level)
Prefer sudo systemctl stop …, then sudo mv /srv/SecBoard_yoursite /srv/SecBoard_yoursite_backup_$(date +%s),
sudo mkdir -p /srv/SecBoard_yoursite, and redeploy a known-good archive — avoid destructive rm -rf when data matters.
Stop worker/beat with Gunicorn when rolling back so no process writes to a half-replaced tree.
DEPLOYMENT_INSTRUCTIONS.md next to PROJECT_SETUP_GUIDE.md in your build package.