/seedkit
Project name: 09-ssh-deploy
Purpose: production app deployed to a remote host over SSH from GitHub Actions, using self-hosted services.
Settings layout: split.
Database: PostgreSQL.
Postgres location: Postgres-in-Docker (`db` + `redis` services in `docker-compose.yml`, port `127.0.0.1:5432` published).
Lint with Ruff: yes.
Test runner: pytest + pytest-django.
Type check (pyright + django-stubs): no.
Pre-commit hooks: no.
Internationalisation (i18n): no.
Custom user model: no.
Auth add-on: none.
Structured logging: yes (`structlog`, JSON in prod / pretty in dev, request-scoped `request_id`).
Task runner: mise.
Add-ons:
- redis
- tasks: Django Tasks with the Redis Queue backend (`django-tasks-rq`). Also `uv run manage.py startapp jobs`, register `jobs` in `INSTALLED_APPS`, wire `jobs/apps.py` `ready()` to import `tasks`, and add a sample `@task` to `jobs/tasks.py`.
- analytics: Umami (self-hosted, env-driven website ID and host)
- email: none (this project does not send transactional mail and the test verifies the skip path).
- CORS: no.
- REST API: none.
- Frontend: none.
- Auth hardening: N/A (auth = none).
- Health check endpoints: yes.
- `robots.txt`: no.
- `django-extensions`: no.
- Devcontainer: no.
Production setup:
- apply Django security settings
- CSP via `django-csp`: yes
- error reporting: Bugsink (self-hosted, sentry-sdk DSN)
- GDPR: PII scrubbing in error reports, retention defaults, user data export/delete
- CI: GitHub Actions test workflow
- deploy: GitHub Actions deploy via SSH (rsync + remote `docker compose pull && up -d`)
- database backups via `django-dbbackup`: yes (self-managed host — no native backup service)
- production Dockerfile: multi-stage — uv builder → `python:3.12-slim-bookworm` runtime
Run the foundation + boot check locally. Generate `Dockerfile`, `docker-compose.prod.yml`, `.github/workflows/test.yml`, `.github/workflows/deploy.yml`. Do not actually deploy — verify all artifacts are present, `docker build .` succeeds, and the deploy workflow references `secrets.SSH_HOST`, `secrets.SSH_USER`, `secrets.SSH_KEY`.
Production app deployed to a remote host over SSH from GitHub Actions, using self-hosted services.
- Django 6, split settings (
config/settings/{base,local,production,test}.py) - PostgreSQL 17, Postgres-in-Docker for local dev (
docker-compose.yml) - Redis — cache + Django Tasks RQ broker
- Django Tasks with the Redis Queue backend (
django-tasks-rq+django-rq); tasks live injobs/tasks.py - Structured logging via
structlog+django-structlog— pretty console in dev, JSON in prod, per-requestrequest_id - Analytics: Umami (self-hosted, env-driven website id and host)
- Health checks:
/healthz(liveness),/readyz(DB reachable) - Lint: Ruff. Tests: pytest + pytest-django. No type checking, no pre-commit hooks, no i18n, no custom user model, no auth add-on.
- Task runner: mise (
mise.toml) - Security: Django's HSTS / secure-cookie / SSL-redirect settings +
django-cspinconfig/settings/production.py - Error reporting: Bugsink (self-hosted, Sentry-protocol) via
sentry-sdk, with PII scrubbing (GDPR) - User data export/delete management commands (
jobs/management/commands/) for GDPR requests - Database backups:
django-dbbackupto an S3-compatible bucket (self-managed host, no native backup service) - CI: GitHub Actions test workflow
- Deploy: GitHub Actions over SSH (rsync-free — builds + pushes a GHCR image, then
docker compose pull && migrate && up -don the host) - Production Dockerfile: multi-stage — uv builder →
python:3.12-slim-bookwormruntime
This dev machine already runs native Postgres (5432) and Redis (6379) outside Docker, so the local docker-compose.yml remaps to avoid bind conflicts:
- Postgres:
127.0.0.1:5435→ container5432 - Redis:
127.0.0.1:6381→ container6379
Adjust DATABASE_URL / REDIS_URL in .env (and the port mappings in docker-compose.yml) if your machine is free of conflicts and you'd rather use the standard 5432 / 6379.
cp .env.example .env # then set a real DJANGO_SECRET_KEY
mise trust && mise install
mise run install
docker compose up -d # db + redis
mise run migrate
mise run superuser
mise run dev # runserver
mise run worker # rqworker default, in a second terminal
mise run test
mise run lint
mise run fmtFallback without mise: uv run manage.py <command> for every task above.
Secrets set in repo settings: SSH_HOST, SSH_USER, SSH_KEY, GHCR_TOKEN (a PAT with read:packages, used by the server to pull private images).
First deploy — create deploy/.env.prod on the host from deploy/.env.prod.example, then:
ssh user@host
cd /srv/09-ssh-deploy
export GITHUB_REPOSITORY=owner/repo
docker compose --env-file deploy/.env.prod -f deploy/docker-compose.prod.yml pull
docker compose --env-file deploy/.env.prod -f deploy/docker-compose.prod.yml run --rm web python manage.py migrate
docker compose --env-file deploy/.env.prod -f deploy/docker-compose.prod.yml up -dAfter that, .github/workflows/deploy.yml handles every push to main: it builds + pushes the image to GHCR, then SSHes in to pull, migrate, and restart. Database backups run via django-dbbackup — schedule dbbackup / mediabackup with a cron line on the host (see references/dbbackup.md in the seedkit skill for the exact crontab).
Built with Seedkit.