Skip to content

Deployment

Architecture

Browser → Host Nginx (TLS) → ses_proxy → ses_server (Django)

Each institute = PostgreSQL schema + DomainRoutingMiddleware

URL structure (path-based routing):
  /<institute>/admin/        → Django admin for that institute
  /<institute>/auth/sso/      → SSO login for that institute
  /<institute>/exam-entry/    → Exam entry for that institute
  /<institute>/manuals/       → Manuals for that institute
  /health                     → Health check (public)

Docker Services

Service Count Port Description
ses-server 1 9000 Django (Gunicorn)
ses-proxy 1 9010 OpenResty + Lua token validation
postgres 1 5432 PostgreSQL 16
keycloak-postgres 1 5432 PostgreSQL 16 (Keycloak)
keycloak 1 8080 Keycloak OIDC provider
odyssey 1 6432 PostgreSQL connection pooler
redis 1 6379 Redis 7 (cache/sessions)

Local Deployment

# 1. Clone the deployment repo
cd /mnt/c/Users/evos/dev/ses-ws/deploy-local

# 2. Generate secrets
./generate/generate_secrets.sh <your-github-pat>

# 3. Run the Ansible playbook
ansible-playbook -i inventory.ini playbook.yml

The playbook: 1. Installs Docker, Nginx, mkcert 2. Creates ses service user 3. Clones the ses repo to /opt/ses 4. Generates TLS certificates 5. Configures Nginx as reverse proxy 6. Builds and starts all containers 7. Runs Django migrations and setup 8. Seeds Keycloak IdentityProvider

Tenant Management

# List all tenants
python manage.py list_tenants

# Add a new tenant (creates schema, route, institute, IdP)
python manage.py add_tenant <slug> "<Name>"

# Delete a tenant (drops schema, removes route)
python manage.py delete_tenant <slug>

After adding a tenant, it is immediately accessible at https://<domain>/<slug>/admin/ — no rebuild needed.\n\n## Scheduled Maintenance\n\n### Expired SSO Session Cleanup\n\nSSO sessions created during authentication expire over time. To comply with data-retention requirements and avoid database growth, remove expired sessions on a schedule with the cleanup_expired_sso_sessions management command. It iterates over all tenant schemas automatically, so no per-tenant setup is required.\n\nRun it manually to verify:\n\nbash\n# Preview what would be deleted (no changes)\npython manage.py cleanup_expired_sso_sessions --dry-run\n\n# Delete expired sessions older than 30 days\npython manage.py cleanup_expired_sso_sessions\n\n# Also delete orphaned user accounts (no password, no sessions, not superuser)\npython manage.py cleanup_expired_sso_sessions --delete-orphan-users\n\n\nOptions:\n\n| Option | Default | Purpose |\n|--------|---------|---------|\n| --dry-run | off | Show what would be deleted without changing anything |\n| --delete-orphan-users | off | Also delete Django user accounts with no usable password, no remaining SSO sessions, and no superuser flag |\n| --max-age-days N | 30 | Only delete sessions that expired more than N days ago |\n\nScheduling with cron — run it daily via the server\'s cron (e.g. every night at 3:00):\n\nbash\n# Add to crontab with: crontab -e\n0 3 * * * cd /opt/ses && docker compose exec ses-server python manage.py cleanup_expired_sso_sessions --delete-orphan-users >> /var/log/ses-sso-cleanup.log 2>&1\n\n\nNotes:\n- The command must run inside the ses-server container, so the docker compose exec path is required.\n- It loops over every tenant schema automatically; a total summary is printed at the end.\n- --delete-orphan-users only removes users with no usable password and no remaining SSO sessions, so staff/superuser accounts are never touched.\n\n## Sync Code from Windows (No Rebuild)

wsl bash -c 'cd /opt/ses && rsync -az --delete /mnt/c/.../src/ server/src/ && docker compose restart ses-server-1'