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'