The GP Booking App (gp_booking_app) is a Django 5.2 multi-tenant healthcare platform serving GP practices, dental surgeries, and partner organisations. It is designed to meet NHS DSPT/DTAC Category 3 compliance requirements.
Live URL: https://gp.veripath.co.uk (served from VPS2 via VPS1 nginx)
Codebase: /root/work/gp_booking_app/ — VPS1 holds the development source; VPS2 runs the live stack (still a hand-copied tree — see "Still open" below)
Ops config: compose files, host scripts, systemd units and nginx sites are versioned at /root/work/veripath-ops (private Forgejo repo matthew/veripath-ops)
Restructure: the 3-VPS split was re-arranged on 2026-09-11 — see VPS Re-Arrangement 3; earlier history in 3-VPS Restructuring Plan
Scope note: this page documents the booking platform, whose repository, image and
container are all namedgp_booking_app. It serves more than GP — dental tenants run a
separate stack (dental_booking_app) and the pharmacy and care-home sectors are planned.
App-specific detail lives at Dental App — Overview.
The name is historical and internal; no rename is planned.
¶ ✅ CURRENT STATUS — 2026-09-11 (read this first)
The estate was re-arranged on 2026-09-11. The plan, its evidence and the verification
recipe live at VPS Re-Arrangement 3 — that page is
the authority for host placement; this page describes the application itself.Running layout:
- VPS1 (88.208.212.211) — gateway + data + AI: nginx/TLS (every vhost), central
Postgresgp_booking_postgres,client_vps_postgres— the canonical client tenant
DB, started by bring-up step 4b, Keycloak, MinIO, LiveKit, Wiki.js, Forgejo, AES ×4,
Netdata, patient portal, mail and the security dashboard. It runs no application
runtime.- VPS2 (212.227.199.114) — the application engine, and the only host running app
runtimes:gp_booking_app(Daphne :8000) anddental_booking_app(Daphne :8010),
each with a Celery worker/beat and one Redis, plus Orthanc (dental DICOM) and MailHog.
Reached from VPS1 viatun-apps-vps2.service; every database via
tun-vps1.services.service.- VPS3 (77.68.112.126) — auxiliary: DefectDojo (
tun-aux-vps3:8090) and ERPNext
(erpnext-deploy, :8005) serving the test-client library client-VPSs
(test-client.accounts.gp/test-client.accounts.dental), whose ERPNext DBs live on
each client's own VPS (SIAAS model).What changed on 2026-09-11 (detail in the change log at the foot of this page):
- Dental has one home: VPS2, built from git. The duplicate VPS1 runtime is retired
(containers stopped, not deleted — rollback intact) and the temporary:8011
AI-radiography override is gone. All three dental consumers —dental.veripath.co.uk,
test-client-dental.dental.and the/api/path ontest-client-dental.patient.—
are served by the VPS2 deployment. The two hosts' DICOM stores were reconciled
through Orthanc's API: 10 studies / 5 patients each, all synthetic test imagery.- Ops config is under version control at
/root/work/veripath-ops(105 files:
/opt/infra,/opt/scripts, the bring-up and tunnel units, host nginx sites and
crontabs), pushed to the private Forgejo repomatthew/veripath-ops. Credentials are
excluded by construction. The live files under/optare still what runs — pull
before editing one and commit the change in the same window.- GP's stranded work is committed and pushed —
mainat7b1661f, including the
four previously untracked migrations. Working tree clean;manage.py checkpasses.- The VPS1 CPU burn is fixed. Netdata's per-container Docker collection was costing
~1.8 of VPS1's 4 cores; it now runs at a 10 s interval through a mounted config (held
in the ops repo). dockerd 86% → ~10%, containerd 88% → ~9%, VPS1 load 5.0 → 0.7.- Boot-time bring-up works again — the patient-portal container-name conflict is
guarded and non-fatal, theclient-postgrescompanion is started and remote stacks
retry:systemctl status infra-bringup= active (exited 0), restart loop gone.- Security remediation (see Security & Compliance): the shared NHS signing key was
committed in a publicly readable Forgejo repository and has been rotated; hardcoded
credentials were removed from tracked source; the ORTHANC webhook token and the
Keycloakdental-booking-appclient secret were rotated; both app repos had key
material purged from git history and were force-pushed, with a GC on the Forgejo side.
The dental repository is now private.Still open (2026-09-11):
- Central database password rotation — the same value sits in
/opt/infra/.env,
/opt/cold-standby/docker-compose.yml,/opt/wagtail/docker-compose.yml,
/etc/default/gp-booking-celeryand compiled scripts, so it needs a planned window.- Migration-history cleanup — orphaned recorder entries (for example
integrations 0010_practice_mailbox_modeland
0014_agentknowledgebase_category_and_more) mean a fresh test database still cannot be
built. The reconstructed0013_add_lynis_hardening_indexis committed, but the
recorder table itself needs tidying.- GP still runs from a hand-copied tree on VPS2 rather than a clone — the last piece
of the re-arrangement's Phase 2, and the exact mechanism by which the dental duplicate
arose. Until it lands, the GP deployment can drift again.- A second
client_vps_postgresruns on VPS2 (bound to host-local127.0.0.1:5435,
up since 2026-08-16, holding its owndsp_clinic_dev). The apps do not use it:
containers reach the canonical VPS1 copy through the tunnel bind at172.18.0.1:5435.
Reconcile or retire it.- Delete the root-only pre-purge mirrors
(/root/backups/{dental,gp}_booking_app-pre-purge-20260911.git) once the purge is
confirmed good.Background: VPS2's containers vanished 2026-08-14 16:38. Root cause
established 2026-08-16: an SSH session from VPS1 randocker compose down+
docker system prune -a --volumes --forceon VPS2 (bash history + journalctl
confirm), pruning the GP app images and both external docker networks. Not a
crash and not data loss. Apps were recovered onto VPS1 on 08-15 as an emergency
fallback, then the split was restored on 08-16 with freshly built images. A
container-count watchdog on VPS2 (/opt/scripts/container-watchdog.sh, 5 min)
now catches any repeat in minutes.Repeat-outage protection (added 2026-08-16, same session): VPS2 now
self-heals viavps2-stack-guard.service(timer, every 2 min) — it
recreates the external docker networks if missing and re-runs
docker compose up -d --no-buildfor both booking-app stacks if any
required container is down. A shell guard/etc/profile.d/vps2-guard.sh
blocksdocker compose downanddocker system pruneon VPS2 (use
docker compose stop). See AGENTS.md "⚠️ NEVER run these on VPS2".Config note:
bring-up.sh, the tunnel units and this page reflect the
post-re-arrangement layout; the config files behind them are versioned at
/root/work/veripath-ops.
Since this page was last refreshed (2026-10-11): the five client-facing
tenant-gateway@socat
instances on VPS1 — the client databases plus the dental client's object storage (5435 / 5436 / 5438 /
5439) and the client box's Orthanc (8043) — were stopped and disabled, because each republished a
client-side port on a Docker bridge address that every container on that host could reach.
Nothing about the application changed: the tenant path was already the tunnel's landing on the
primary's loopback, no tenant record, setting or line of code moved, and the tenants, the patient portal
and the client's imaging system were all re-verified afterwards. Only@8000— the patient portal's
/api/path — remains. Details in the changelog at the foot of this page.
The platform is split across three VPS instances after the restructuring. Inter-VPS
traffic uses persistent SSH tunnels over port 22 (autossh/systemd) — the provider
blocks inbound UDP, so WireGuard cannot be used.
┌──────────────────────────────────────────────────────────────────────────────┐
│ Users (Browsers) / Internet │
│ gp.veripath.co.uk │ dental.veripath.co.uk │ test-client.* │ aes.* │ patient.* │
└──────────────────────────┬───────────────────────────────────────┬───────────┘
│ HTTPS │
┌──────────────────────────▼────────────────────────────────────┐ │
│ VPS1 (88.208.212.211) — Gateway + Data + AI │ │
│ nginx (host-level, SSL termination) + ModSecurity │ │
│ gp_booking_postgres (central DB: dsp_clinic, veripath_dental,│ │
│ wikijs, keycloak, etc.) │ │
│ Keycloak, MinIO, LiveKit, Netdata, Wiki.js, Forgejo, │ │
│ Roundcube, AES (PKI/TSA/Signer/Portal), security-dashboard, │ │
│ patient-portal-pwa, client_vps_postgres (canonical DB) │ │
└─────────┬─────────────────────────────────────────────────────┴───┬┘
│ SSH tunnels (autossh, port 22) │ SSH tunnel
│ 127.0.0.1:8000 / 8010 (apps → VPS2) │ (8090)
│ 172.18.0.1:5433 (central PG) │
│ 172.18.0.1:8004 (AES) / 5436 / 5437 (client DBs) │
┌─────────▼──────────────────────────────────────────────────────┐ │
│ VPS2 (212.227.199.114) — Core Booking Apps │ │
│ gp_booking_app stack: app (Daphne ASGI) │ │
│ celery_worker/beat (--concurrency=2), gp_booking_redis, │ │
│ mailhog (dev mail capture) │ │
│ dental_booking_app stack: dental_app (Daphne), │ │
│ dental_celery_worker/beat (--concurrency=2), │ │
│ dental_redis, dental_orthanc │ │
│ DB: central via tunnel; tenants via 172.18.0.1:<port> │ │
└────────────────────────────────────────────────────────────────┘ │
│
┌────────────────────────────────────────────────────────────────────▼┐
│ VPS3 (77.68.112.126) — Auxiliary │
│ DefectDojo (security scanning) │
│ ERPNext (client accounting, :8005) │
└───────────────────────────────────────────────────────────────────┘
Connectivity (SSH tunnels, autossh/systemd — the provider blocks inbound UDP):
127.0.0.1:8000 GP, 127.0.0.1:8010 dental), central Postgres172.18.0.1:5433), client tenant DB (172.18.0.1:5435 → VPS1's canonicalclient_vps_postgres), AES portal (172.18.0.1:8004) and the client-VPS forwards5436 dental client DB; 5437 stale — dev-client is mothballed and nothing listens on5438 dental client object storage and 5439 pharmacy client DB travel on their127.0.0.1:8090) and ERPNext (127.0.0.1:8005).tenancy point at 172.18.0.1:<port> (each host's Docker bridge172.18.0.1:5435 is the tunnel,/var/www/gp_booking_app_static/ and/var/www/dental_booking_app_static/, synced from VPS2 every 10 min/opt/scripts/sync-static.sh).In-app architecture (unchanged by the move): the app is a multi-tenant Django
platform with RBAC + tenant middleware, a sector database router
(tenancy.routers.SectorDatabaseRouter), and modules for telehealth (LiveKit),
insurance billing, lab documents, and an AI sidebar.
| Component | Technology |
|---|---|
| Framework | Django 5.2 (Python 3.11) |
| Database | PostgreSQL 15 (central on VPS1, reached via SSH tunnel) + client_vps_postgres (on VPS2) + per-tenant DBs |
| Cache/Broker | Redis 7 / Valkey 7.2 (per-stack on VPS2) |
| Task Queue | Celery + Celery Beat (--concurrency=2 on VPS2) |
| Web Server | Nginx 1.24 (host, VPS1) + Daphne (ASGI, container on VPS2) |
| Container Runtime | Docker Compose (all stacks) |
| Auth | django-oidc + Keycloak 26.6.1 |
| API | Django REST Framework |
| Encryption | django-encrypted-model-fields (Fernet) |
| Security Scanning | Lynis, DefectDojo, Netdata |
| Code Repository | Forgejo 14.0 (git.veripath.co.uk) |
| Documentation | Wiki.js (wiki.veripath.co.uk) |
| Telehealth | LiveKit Server + Telehealth Django Module |
| Digital Signing | AES PKI, TSA, Signer, Portal |
| Accounting | ERPNext 15 (Frappe) |
| Monitoring | Netdata (VPS1) |
| Object Storage | MinIO (S3-compatible, VPS1) |
| VPS3 | 77.68.112.126 — auxiliary (DefectDojo); ERPNext deferred |
The application uses a multi-database architecture with a custom SectorDatabaseRouter for data segregation. The central PostgreSQL 15 instance (gp_booking_postgres) lives on VPS1 (host port 5433) and is reached from the apps on VPS2 via an SSH tunnel (172.18.0.1:5433). Per-client tenant databases run on client_vps_postgres, which lives on VPS1 and is reached from VPS2 containers at 172.18.0.1:5435; per-client VPS DBs are reachable at 172.18.0.1:5436 — dental — and 5437, which is stale (the dev-client tenant is mothballed and no listener exists on the primary), also tunnelled to VPS1. A second, same-named container runs locally on VPS2 bound to 127.0.0.1:5435 — the apps do not use it (see Known Issues).
dsp_clinic (default) — GP SectorThe primary database holding all GP-related data. Contains the majority of application tables.
⚠️ Configuration note (2026-07-01): The
env.productionfile was previously misconfigured to point atdsp_clinic_devinstead ofdsp_clinic. Thedsp_clinic_devdatabase is a development/corrupted copy missing most application tables (e.g.onboarding_practicedid not exist). Always ensureDB_NAME=dsp_clinicin production config. See the infrastructure fix log for details.
veripath_dental — Dental SectorSeparate database for dental practice data, including:
dental_dentalpractice — Practice detailsdental_dentalprovider — Clinician/provider records (GDC-registered)dental_dentalappointment — Dental appointmentsdental_fp17claim — NHS FP17 claim formsdental_dentaltransaction — Payment transactions (Stripe)dental_dentalclinicalnote — Clinical notes with tooth mappingdental_dentalscheduleblock — Provider schedule blocksUsed by the infrastructure app and client_monitor app:
infrastructure_clientcapacitysnapshot — DB, disk, CPU, memory metrics per tenantinfrastructure_clienttunnelhealth — WireGuard tunnel health checksinfrastructure_clientbackuplog — Backup status trackingNote: The GP and Dental databases share the same PostgreSQL instance but are separate logical databases. The
SectorDatabaseRouterroutes queries based on Django model app labels. Theclient_monitorapp contains unmanaged (managed = False) models that mirror theinfrastructureapp's models — these are used for reading from external client VPS databases via the database router. Client VPS databases are hosted on the secondaryclient_vps_postgresinstance.
The shared PostgreSQL instance hosts the following databases:
| Database | Owner | Purpose |
|---|---|---|
dsp_clinic |
postgres | GP booking app (primary — in use) |
dsp_clinic_dev |
postgres | Dev/staging (partial data — NOT production) |
veripath_dental |
postgres | Dental sector data |
veripath_global |
postgres | Global/shared data |
keycloak |
keycloak | Keycloak auth state |
wikijs |
wikijs | Wiki.js content |
aes_portal |
postgres | AES signing portal |
directus |
postgres | Retired CMS |
test_dsp_clinic |
postgres | Testing |
Additional databases on separate PostgreSQL instances:
forgejo — Forgejo git server (forgejo_db container on VPS1, alongside forgejo_redis)defectdojo — DefectDojo AppSec (defectdojo_postgres container on VPS3)erpnext — ERPNext accounting (erpnext_veripath_postgres container on VPS3)dsp_clinic/, core/)tenancy.middleware.TenantMiddleware)tenancy/)Sector ──1:N──> Tenant
tenancy.utils provides set_current_tenant(), get_current_tenant(), with_tenant() context manager for Celery tasks and non-HTTP code pathssector_client_test-clinic) with fallback chainusers/)CustomUser (AbstractUser)
├── role: PATIENT | CLINICIAN | RECEPTIONIST | PRACTICE_MANAGER | ADMIN
├── Dental roles: DENTAL_ADMIN, DENTAL_CLINICIAN, DENTAL_HYGIENIST, etc.
├── Partner roles: ORG_ADMIN, PARTNER_ADMIN, PARTNER_REGISTERED_MANAGER, etc.
├── nhs_number (encrypted)
├── phone_number, address, postcode (encrypted)
├── date_of_birth (encrypted)
├── sex (encrypted), emergency_contact_name, emergency_contact_phone (encrypted)
├── verification_level: P5 / P9 / UNVERIFIED
├── org_id (partner org scoping)
├── prescriber_type + professional_registration_number
└── legacy_id (legacy system import)
Supports 20+ roles across GP, Dental, and Partner sectors. All PII encrypted at rest. is_staff is auto-set on save based on role (clinical/admin roles get staff access). REST API endpoints (users/api/) provide user CRUD, patient search, and role management. Patient proxy model and bulk import command (import_patients) available.
appointments/)Appointment ──> Patient (CustomUser)
──> Clinician (CustomUser)
──> Clinic (slot_management)
──> AppointmentType
──> ReminderLog (notifications)
──> PaymentTransaction (1:N)
──> Invoice / InvoiceLineItem
──> InsuranceInvoice (via insurance_billing)
ProviderSession ──> Clinician
──> Clinic
──> AppointmentTypes (M2M)
BookingConfig ──> Practice (1:1)
AppointmentMessage ──> Practice
Key features:
reason fieldPaymentConfig model per practice (encrypted keys), PaymentTransaction (CASH, CARD, BANK_TRANSFER, CHEQUE, STRIPE), webhooks for payment confirmationInvoice and InvoiceLineItem models with PDF generation and email delivery. Insurance billing queue for claim submission.clinical_data/)ClinicalNote ──> Appointment
──> Author (Clinician)
Prescription ──> Patient
──> Prescribing Clinician
──> Appointment (optional)
──> PrescriptionAuditLog
PatientHealthProfile ──> Patient (1:1)
PatientProfile ──> Patient (1:1)
──> PartnerOrg (optional)
──> PatientMedication (1:N)
──> PatientCondition (1:N)
──> PatientAllergy (1:N)
──> PatientConsent (1:N)
PatientImage ──> Patient
──> Appointment (optional)
SnomedICD10Map — SNOMED CT → ICD-10 cross-map
DrugSafetyRule — Drug-disease contraindication alerts
GPNotificationLog — GP surgery notification logs
Key features:
SnomedICD10Map for automated insurance/coding cross-referenceDrugSafetyRule for drug-disease contraindication alerts at prescribing time with BLOCK/WARN severityslot_management/)Clinic ──1:N──> SlotTemplate
pcn_booking/)PCNClinic ──> Clinic
dental/)DentalPractice ──1:N──> DentalProvider
──1:N──> DentalAppointment
──1:N──> DentalRecallRule
──1:N──> DentalAppointmentCategory
──1:1──> DentalPaymentConfig
DentalAppointment ──> Patient
──> DentalProvider
──> DentalPractice
──1:1──> FP17Claim
──1:N──> DentalClinicalNote
──1:N──> DentalTransaction
DentalScheduleBlock ──> DentalPractice
──> DentalProvider
──> DentalAppointmentCategory (zone)
Key features:
partner/)Organisation ──1:1──> PartnerOrg
──1:N──> OnboardingAuditLog
PartnerOrg ──1:N──> BreachReport
──1:N──> MedicalDevice
──1:N──> PatientProfile (via clinical_data)
Key features:
create_partner_realm command using partner-realm-blueprint.json (clients, roles, authentication flows)insurance_billing/)PrivateInsuranceInvoice ──> Appointment (1:1)
PatientPaymentMethod ──> Patient
──> Practice
ClaimExportBatch ──> PrivateInsuranceInvoice (M2M)
Key features:
lab_documents/)ClinicLabConfig ──> Practice (1:1)
LabReportMailbox ──> ClinicLabConfig
LabDocument ──> Patient (optional)
LabReport ──> LabDocument
──> Patient
LabResult ──> LabReport
──> Biomarker
Key features:
telehealth/)VideoRoom ──> Appointment (1:1)
VideoSessionLog ──> VideoRoom
──> Participant (User)
Key features:
VideoRoom status workflow (WAITING → ACTIVE → ENDED)frontend/telehealth/ with components for participant tiles, control bar, waiting screen, consultation notes, call summarystatic/telehealth/telehealth.veripath.co.uk:7880 (WebSocket proxy)patient_portal/)Conversation ──> Patient
──> Practice
Message ──> Conversation
──> Sender (User)
PushSubscription ──> Patient
PrescriptionDispenseLedger ──> Prescription (by ID)
Key features:
onboarding/)ICB ──1:N──> Practice
Practice ──1:N──> StaffRoleRecord
──1:N──> Invitation
──1:N──> InformationAssetRegister
──1:N──> RecordOfProcessingActivity (ROPA)
──1:N──> RetentionTimetable
──1:N──> PolicyAcknowledgment
──1:N──> PolicyDocument (via M2M)
StaffRoleRecord ──> User
──> Practice
──1:N──> StaffOnboardingAudit
RiskManagementRegister (system-wide, optional partner org link)
Comprehensive DSPT compliance features:
staff_management/)LeaveType ──> Practice
LeaveRequest ──> StaffRoleRecord
──> LeaveType
LeaveBalance ──> StaffRoleRecord
──> LeaveType
PracticeTeam ──> Practice
TeamMembership ──> PracticeTeam
──> StaffRoleRecord
notifications/)ReminderLog ──> Appointment
dashboards/)FormalRole (SIRO, CLINICIAN, RECEPTIONIST, etc.)
UserRole ──> User
──> FormalRole
Dashboard (PATIENT, RECEPTIONIST, CLINICIAN, PRACTICE_MANAGER, etc.)
DashboardPermission ──> FormalRole
──> Dashboard
Eight dashboards:
| Dashboard | Data Category | Primary Users |
|---|---|---|
| Patient Dashboard | Clinical | Patients |
| Receptionist Dashboard | Administrative | Receptionists |
| Clinician/GP Dashboard | Clinical | Clinicians |
| Practice Manager Dashboard | Administrative | Practice Managers |
| ICB/Regional Admin Dashboard | Commercial | ICB Admins |
| Super-User/System Admin Dashboard | System | System Admins |
| Accounts & Contract Management | Commercial | Billing Admins |
| Compliance Administration | Administrative | Compliance Officers |
Data segregation enforced: clinical roles cannot access commercial dashboards and vice versa.
Additional dashboards in the partner module:
auditing/)AuditLog ──> User (optional)
clinical_safety/)HazardLog (DCB0129)
reporting/)PlatformActivityEvent ──> Practice
──> User (optional)
MonthlyActivitySummary ──> Practice
CQCAnalyticsCache ──> Practice
ComplianceActivity ──> Practice
──> PartnerOrg (optional)
ICBContract model has been removedintegrations/)NHSIntegrationConfig (GP_CONNECT, PDS, EPS, NHS_NOTIFY, AES)
EmailIntegrationConfig (SMTP, NHSmail)
AgentConfig ──> User (optional), capabilities (JSON)
UserImapAccount ──> User (optional)
LynisScanResult ──> User (optional)
UserImapAccount model with OAuth2 token management (Microsoft Graph), mailbox search, compose/sendai_sidebar/)NoteMapping (URL pattern → note path)
SidebarNote (cached rendered HTML)
SidebarSettings (key-value config)
infrastructure/)ClientCapacitySnapshot ──> Tenant
ClientTunnelHealth ──> Tenant
ClientBackupLog ──> Tenant
The infrastructure app provides a per-tenant client VPS dashboard at /infrastructure/. It displays:
client_monitor/)ClientCapacitySnapshot (unmanaged, reads external VPS DB)
ClientTunnelHealth (unmanaged, reads external VPS DB)
ClientBackupLog (unmanaged, reads external VPS DB)
Contains unmanaged (managed = False) models that mirror the infrastructure app's models. These are used for reading from external client VPS databases via the database router connected through WireGuard tunnels. Enables the principal VPS to display infrastructure metrics from client VPS instances.
Beyond the GP Booking App, the principal VPS hosts additional services, all running as Docker containers on the gp_booking_app_gp_booking_network:
| Service | Container | Network IP | Host Port | URL / Purpose |
|---|---|---|---|---|
| Wiki.js | wikijs |
172.18.0.11 | 3000 | wiki.veripath.co.uk |
| Forgejo | forgejo |
172.18.0.7 | 3000 | git.veripath.co.uk |
| Keycloak | keycloak |
172.18.0.18 | 8443→8080 | auth.veripath.co.uk |
| PostgreSQL (main) | gp_booking_postgres |
— | 127.0.0.1:5433→5432 | All databases (VPS1; VPS2 apps reach it via tunnel 172.18.0.1:5433) |
| PostgreSQL (client VPS) | client_vps_postgres |
— | 172.18.0.1:5435→5432 | Client VPS databases (on VPS2) |
| Redis | redis |
172.18.0.17 | 6379 | Celery broker (moved to VPS2) |
| MailHog | mailhog |
172.18.0.5 | 1025/8025 | Dev email capture |
| LiveKit | livekit |
host | 7880-7881, 3478/udp | Telehealth video (telehealth.veripath.co.uk) |
| Roundcube | roundcube |
172.18.0.24 | 8082→80 | mail.veripath.co.uk |
| Patient Portal PWA | patient-portal-pwa |
172.22.0.7 (dev net) | 8081→80 | patient.veripath.co.uk |
| AES PKI | aes-pki |
host | 8001 | Certificate authority |
| AES TSA | aes-tsa |
host | 8002 | Timestamp authority |
| AES Signer | aes-signer |
host | 8003 | Digital signing |
| AES Portal | aes-portal |
172.18.0.14 | 8004 | aes.veripath.co.uk |
| Security Dashboard | security-dashboard |
172.18.0.8 | 8006 | security.veripath.co.uk |
| DefectDojo | multiple | — | 8090 (VPS3) | AppSec vulnerability mgmt (moved to VPS3) |
| MinIO | minio |
172.18.0.6 | 9000 | S3 object storage |
| Netdata | netdata |
host | — | System monitoring |
| ERPNext | erpnext_web, erpnext_worker, erpnext_scheduler |
— | 8005→8000 | accounts.veripath.co.uk (stopped / deferred) |
| pgAdmin | — | host | 5050 | pgadmin.veripath.co.uk database management |
| Celery Beat | gp_booking_celery_beat |
— | — | Periodic task scheduler |
| Integration | Purpose | Endpoint |
|---|---|---|
| GP Connect | Patient record retrieval (FHIR) | clinical_data/integrations/gp_connect.py |
| PDS | Personal Demographics Service | Configured in settings |
| EPS | Electronic Prescription Service | clinical_data/integrations/eps_fhir.py |
| NHS Notify | Appointment reminders | services/nhs_notify.py |
| NHS OAuth2 | Authentication | clinical_data/integrations/nhs_auth.py |
All integrations operate in sandbox mode by default (USE_MOCK_EPS = True).
config/secrets/, with *.pem/*.key gitignored); no credentials in tracked source (gitleaks-swept 2026-09-11)integrations appThe NHS signing key pair for the EPS/NHS auth leg was found committed to both app
repositories, and the dental repository was publicly readable on Forgejo behind an
internet-facing vhost with no IP allowlist. What was done:
config/secrets/nhs/nhs_private_key.pem — untracked, mode 600 — with a legacy rootnhs_auth.py / validate_eps_config.py resolve it there. The old key wasDENTAL_OIDC_CLIENT_ID/_SECRET), the AEStemp_settings.py / test_key.py.Tenant row andconfig/secrets/orthanc.env, container recreated, verified end-to-end through to Celerydental-booking-app client secret.git filter-repo across all branches andreflog expire + gc --prune=now on the Forgejo side: the*.pem / *.key are gitignored, .dockerignoreconfig/secrets/ and key material, and the dental repository is now private.REDISCLI_AUTH + grep -q PONG —Still open: the central database password has not been rotated (the value is shared
across five host locations — see the status block), and secrets are still resolved from env
files on disk rather than a secret store. Root-only pre-purge mirrors are retained at
/root/backups/{dental,gp}_booking_app-pre-purge-20260911.git until the purge is confirmed.
All sensitive patient and staff data is encrypted at rest using django-encrypted-model-fields (Fernet symmetric encryption):
gp_booking_postgres), client_vps_postgres (canonical client tenant DB), Keycloak,gp_booking_app anddental_booking_app stacks (app + celery worker/beat + one Redis each), Orthanc (dental:8090) and ERPNext (:8005, clientgp_booking_redis still runs here./etc/nginx/sites-enabled/gp_booking_app_gp_booking_network (172.18.0.0/16, bridge) on| Service | Container | Image | Port Mapping |
|---|---|---|---|
| Daphne App (VPS2) | gp_booking_app |
gp_booking_app-app |
127.0.0.1:8000→8000 |
| Celery Worker | gp_booking_celery_worker |
gp_booking_app-celery_worker |
None |
| Celery Beat | gp_booking_celery_beat |
gp_booking_app-celery_beat |
None |
| PostgreSQL (main) | gp_booking_postgres |
pgvector/pgvector:pg15 |
127.0.0.1:5433→5432 |
| PostgreSQL (client tenant DB) | client_vps_postgres — canonical copy on VPS1 |
postgres:15-alpine |
127.0.0.1:5435→5432 on VPS1; bound at 172.18.0.1:5435 by the tunnel on VPS2 |
| Redis (one per stack) | gp_booking_redis, dental_redis |
redis:7-alpine |
None |
| Keycloak | keycloak |
quay.io/keycloak/keycloak:26.6.1 |
127.0.0.1:8443→8080 |
| MailHog | mailhog |
mailhog/mailhog:latest |
1025, 8025 |
| ERPNext Web | erpnext_web |
erpnext_v15:latest |
127.0.0.1:8005→8000 |
| ERPNext Worker | erpnext_worker |
erpnext_v15:latest |
None |
| ERPNext Scheduler | erpnext_scheduler |
erpnext_v15:latest |
None |
| ERPNext PostgreSQL | erpnext_client_vps_pg |
postgres:15-alpine |
127.0.0.1:5436→5432 |
The ERPNext rows above run on VPS3 and are reached from VPS1 through the tunnel (the
host port is what nginx proxies to). The client tenant DB row is the canonical copy on
VPS1 — VPS2 also runs a same-named container bound to host-local127.0.0.1:5435that the
apps do not use (see Known Issues).
⚠️ Legacy (pre-restructure): the standalone
docker runcommands below use the
old/var/www/html/gp_booking_apppath and Gunicorn. The app now runs via
docker compose on VPS2 (/root/work/gp_booking_app/docker-compose.yml) with
Daphne (ASGI). Keep these for reference only.
# Start the app (standalone docker, not docker-compose):
docker run -d --name gp_booking_app \
--network gp_booking_app_gp_booking_network \
-p 127.0.0.1:8000:8000 \
-v /var/www/html/gp_booking_app:/app \
-v /var/www/html/gp_booking_app/staticfiles:/app/staticfiles \
-v /var/www/html/gp_booking_app/media:/app/media \
--restart unless-stopped \
--env-file /var/www/html/gp_booking_app/config/secrets/env.production \
gp_booking_app_msal
# Management inside container
docker exec gp_booking_app python manage.py migrate
docker exec gp_booking_app python manage.py createsuperuser
docker exec gp_booking_app python manage.py update_agent_prompts
docker exec gp_booking_app python manage.py create_partner_realm
docker exec gp_booking_app python manage.py seed_insurance_billing
docker exec gp_booking_app python manage.py import_patients
# Deploy veripath.co.uk website
/opt/deploy-veripath-site.sh
# View app logs
docker logs gp_booking_app
# Celery worker (separate container)
docker run -d --name gp_booking_celery_worker \
--network gp_booking_app_gp_booking_network \
-v /var/www/html/gp_booking_app:/app \
--restart unless-stopped \
--env-file /var/www/html/gp_booking_app/config/secrets/env.production \
gp_booking_app_msal \
celery -A dsp_clinic worker --loglevel=info
# Celery beat (separate container)
docker run -d --name gp_booking_celery_beat \
--network gp_booking_app_gp_booking_network \
-v /var/www/html/gp_booking_app:/app \
--restart unless-stopped \
--env-file /var/www/html/gp_booking_app/config/secrets/env.production \
gp_booking_app_msal \
celery -A dsp_clinic beat --loglevel=info
Nginx runs on the Docker host. Key upstream targets:
| Domain | Upstream | Service |
|---|---|---|
gp.veripath.co.uk |
127.0.0.1:8000 |
Django App (GP) |
*.gp.veripath.co.uk |
127.0.0.1:8000 |
Per-tenant subdomains (wildcard) |
dental.veripath.co.uk |
127.0.0.1:8010 |
Dental app — the VPS2 runtime via tunnel (the old :8011 demo override is retired) |
test-client-dental.dental.veripath.co.uk |
127.0.0.1:8010 |
Dental app (tenant host) |
test-client-dental.patient.veripath.co.uk |
127.0.0.1:8010 (/api/) + 127.0.0.1:8081 |
Dental patient portal + radiograph API |
manuals.dental.veripath.co.uk |
static /var/www/manuals-dental + 127.0.0.1:3053 |
Dental manuals site (auth-gated) |
test-client.gp.veripath.co.uk |
127.0.0.1:8000 |
Django App |
patient.veripath.co.uk |
127.0.0.1:8000 (app) + 127.0.0.1:8081 (PWA) |
Patient Portal |
telehealth.veripath.co.uk |
127.0.0.1:7880 |
LiveKit WebSocket proxy |
auth.veripath.co.uk |
127.0.0.1:8443 |
Keycloak |
wiki.veripath.co.uk |
127.0.0.1:3002 |
Wiki.js |
git.veripath.co.uk |
127.0.0.1:3000 |
Forgejo |
mail.veripath.co.uk |
127.0.0.1:8082 |
Roundcube |
security.veripath.co.uk |
127.0.0.1:8006 |
Security Dashboard |
aes.veripath.co.uk |
127.0.0.1:8004 |
AES Portal |
accounts.veripath.co.uk |
127.0.0.1:8005 |
ERPNext Accounting |
*.accounts.dental.veripath.co.uk |
127.0.0.1:8005 |
ERPNext per-client (e.g. test-client.accounts.dental) |
pgadmin.veripath.co.uk |
127.0.0.1:5050 |
pgAdmin |
gp.protocoldata.co.uk |
gp.veripath.co.uk (alias) |
Domain alias |
veripath.co.uk |
— | Nuxt 3 site (separate container) |
The per-tenant client VPS dashboard is available at:
/infrastructure/infrastructure/views.py — InfrastructureDashboardViewThis system is built around the Sovereign Infrastructure as a Service (SIAAS) model:
autossh/systemd) — the documented standard here; the provider blocks inbound UDP, sosystemctl stop <tunnel-unit> onSee infrastructure/SIAAS/client_vps_setup for the full client VPS provisioning blueprint.
| Issue | Status | Date Fixed |
|---|---|---|
| Wiki-manage.py stale IPs | DB host 172.18.0.7→172.18.0.15 (now 172.18.0.4), API URL 172.18.0.5→172.18.0.11 | 2026-07-01 |
| dsp_clinic_dev vs dsp_clinic | env.production was pointing to corrupted dev DB. Changed to dsp_clinic. | 2026-07-01 |
| Nginx upstream IP stale | App container IP changed on restart. Using host port mapping (127.0.0.1:8000). | 2026-07-01 |
| PostgreSQL/Redis/Keycloak down | All three containers exited simultaneously. Root cause TBD. Restarted. | 2026-07-01 |
| dental.veripath.co.uk upstream stale | Nginx at 172.18.0.6:8000 (old container IP). Fixed to 127.0.0.1:8000. | 2026-07-22 ✅ |
| Patient Portal PWA on separate network | PWA container is on gp_booking_app_dev_gp_booking_network (172.22.0.0/16), not main bridge network. Not a bug but worth noting. | — |
| Duplicate app containers | infallible_varahamihira, gracious_nobel removed. All containers now Compose-managed. |
2026-07-22 ✅ |
| Dental deployed twice against one database | The VPS1 dental runtime was retired (stopped, not deleted) and the :8011 demo override removed; dental's single home is VPS2, built from git. All three consumers cut over and verified. |
2026-09-11 ✅ |
| Netdata's Docker collector burned ~1.8 of VPS1's 4 cores | update_every: 10 for the docker_local collector via a mounted config (in the ops repo). dockerd 86% → ~10%; VPS1 load 5.0 → 0.7. |
2026-09-11 ✅ |
| Boot-time bring-up in a 30-second restart loop | Patient-portal step guarded and non-fatal, client-postgres started, remote stacks retried. Unit now active (exited 0). |
2026-09-11 ✅ |
| Redis password on a healthcheck command line | REDISCLI_AUTH + grep -q PONG pattern applied to both stacks. |
2026-09-11 ✅ |
| Key material and credentials committed to git | NHS signing key rotated, gitleaks sweep, both histories purged and force-pushed with a Forgejo GC, dental repo made private. | 2026-09-11 ✅ |
Expired TLS certificate on patient.veripath.co.uk |
Was the only host using certbot's standalone authenticator (cannot bind port 80 while nginx runs), so renewals failed silently. Re-issued with the webroot authenticator; renews unattended. | 2026-09-11 ✅ |
Duplicate client_vps_postgres on VPS2 |
Bound to host-local 127.0.0.1:5435 only; the apps use the canonical VPS1 copy via the tunnel. Not yet retired. |
OPEN |
| GP runtime is a hand-copied tree on VPS2 | Not a clone, so it can silently drift again. The last piece of the re-arrangement's Phase 2. | OPEN |
| Orphaned migration recorder entries | e.g. integrations 0010_practice_mailbox_model, 0014_agentknowledgebase_category_and_more — a fresh test database cannot be built. Dedicated cleanup needed. |
OPEN |
| Central database password not rotated | Same value in /opt/infra/.env, /opt/cold-standby, /opt/wagtail, /etc/default/gp-booking-celery and compiled scripts — needs a planned window. |
OPEN |
Stray gp_booking_redis on VPS3 |
Running but not referenced by any stack on that host. | OPEN |
Client database aliases use CONN_MAX_AGE = 0 |
tenancy/routers.py opens a fresh PostgreSQL connection per request, and every tenant request crosses two SSH legs (app engine → primary → client box). Not measured — a connection pooler in front of the per-tenant ports is the lever if tenant latency is ever traced here. |
OPEN |
| Client-facing gateway socats on VPS1 | Five tenant-gateway@ instances republished client ports (5435/5436/5438/5439 databases + object storage, 8043 the client's Orthanc) on a Docker bridge address every container on that host could reach. Retired; @8000 (patient portal /api/) kept deliberately. |
2026-10-11 ✅ |
| Change | Details |
|---|---|
| Five gateway instances retired on VPS1 | tenant-gateway@5435, @5436, @5438, @5439 (client databases and the dental client's object storage) and @8043 (the client box's Orthanc) stopped and disabled. Only @8000 remains: the patient portal's nginx proxies its /api/ to it, so that instance is a live path and was deliberately left. |
| Why | Each instance republished a client-side port on a Docker bridge gateway address that every container on VPS1 could reach — that address is now the sandbox bridge. A probe from a throwaway container on the shared network connected to all of them; only PostgreSQL / Orthanc credentials stood in the way, which is exactly the blast-radius property the SIAAS model claims. |
| Why it was safe — no application impact | The tunnel's sshd connects to the port on the primary's loopback, which is the same port the socat forwarded to, so the socat sat beside the path rather than in it. Tenant records still point at the app engine's bridge gateway and the tunnel still binds those addresses there. No code, setting or tenant row changed. |
| Verified afterwards | From a hub-network container: 5435 / 5436 / 5438 / 5439 / 8043 refused, 8000 open. The loopback listeners untouched; the three client tenants' tunnel-health rows written healthy; every tenant hostname answering; the patient portal 200; the client's Orthanc still reachable through the tunnel. The five retired units were reset-failed so monitoring sees no failed unit. |
| Ops repo | veripath-ops d10c139 — the retired template committed with a header naming @8000 as the only sanctioned live use, the phantom socat hop removed from the tun-vps1-dental-minio and pharmacy-tunnel comments, and AGENTS.md's topology lines corrected (including the stale 5437 forward). |
| Documentation | Dated notes on the 2026-08-01 tunnel incident page (its chain diagram), the medical imaging page (8043 identified as the client box's Orthanc) and the SIAAS infrastructure-preparation page. |
| Left deliberately | The stale 5437 forward in tun-vps1.services.service: removing it means restarting the unit that carries the tenant database forwards, for one cosmetic line. Recorded as stale rather than deleted. |
| Change | Details |
|---|---|
| Dental collapsed to one deployment | VPS1's duplicate runtime retired (stopped, not deleted — rollback intact) and the :8011 AI-radiography override removed; VPS2 runs the only dental runtime, deployed from git. dental.veripath.co.uk, test-client-dental.dental. and the /api/ path on test-client-dental.patient. all cut over and verified. |
| DICOM stores reconciled | 9 studies exported from the primary store and imported instance-by-instance via POST /instances (the zip route returns 405 on this Orthanc build). Both stores now report 10 studies / 5 patients; synthetic test imagery, no identifiers printed. |
| Duplicate companions retired (VPS1) | gp_booking_redis, mailhog, dental_redis, dental_ollama, dental_orthanc stopped after proving nothing referenced them. client_vps_postgres deliberately left running and is started by bring-up step 4b. |
| Ops config versioned and pushed | /root/work/veripath-ops (105 files: /opt/infra, /opt/scripts, tunnel/bring-up units, nginx sites, crontabs) → private Forgejo repo matthew/veripath-ops, master = 5a666cc. Credentials excluded by construction. |
| GP stranded work committed | Four logical commits pushed to main (7b1661f), including the four untracked migrations, the integrations-dashboard retirement, hardcoded DB/Redis password removal and a reconstructed 0013_add_lynis_hardening_index. |
| Netdata retuned | Docker/cgroup collection at a 10 s interval via a mounted config — ~1.8 CPU cores returned to VPS1. |
| Bring-up made idempotent and complete | Portal step guarded, client-postgres companion started, remote stacks retried with backoff; full run exits 0 and reaches every step. |
| Disk reclaimed | 17.4 GB of unused images removed (GP builds protected — they were the only build of code not yet in git) after 6.07 GB of build cache was pruned. |
| NHS signing key rotated | A shared RSA-4096 key was committed to both app repos while the dental repo was publicly readable. New pair deployed to both stacks; old key destroyed from every host copy; *.pem/*.key gitignored; .dockerignore excludes config/secrets/. |
| Gitleaks sweep | Removed the dental OIDC client secret, AES client-secret fallback, Trilium ETAPI token, NHS sandbox API key and the scratch files temp_settings.py / test_key.py. |
| Rotations completed | ORTHANC webhook token (both sides, verified end-to-end into Celery ingestion) and the Keycloak dental-booking-app client secret. |
| Git history purged | git filter-repo across all branches and tags in both repos, force-pushed, plus reflog expire + gc --prune=now on the Forgejo side; old key blob unreachable. |
| Repository visibility | dental_booking_app was publicly readable (no IP allowlist on the git vhost) → now private. |
| TLS renewal fixed | patient.veripath.co.uk was serving an expired certificate because it used certbot's standalone authenticator; re-issued with webroot so renewal is unattended. |
| Change | Details |
|---|---|
| Compose migration | All stacks migrated to Docker Compose with shared external network |
| Healthchecks | All services now have healthchecks |
| Resource limits | CPU and memory limits set on all services |
| Log rotation | JSON-file driver with 10MB max-size, 3-file max |
| Secrets | Passwords moved to .env files, never hardcoded |
| Celery broker | Redis configured as Celery broker with password auth |
| Health endpoint | /infrastructure/health/ added to Django app |
| Ghost containers | infallible_varahamihira, gracious_nobel removed |
| Dead containers | 4 exited containers removed |
| Orphaned volumes | 16 hash-named volumes removed (~130MB recovered) |
| Nginx — all internal IPs eliminated | wiki (172.18.0.11), git (172.18.0.7), security/dojo (172.18.0.24), jorja-essex, maple-surgery (172.18.0.6) — all replaced with 127.0.0.1 |
| Wiki.js port | Moved to 127.0.0.1:3002 (previously no host port) |
| DefectDojo | Added 127.0.0.1:8080 host port mapping |
| LiveKit config | Fixed — added keys section |
| patient-portal-pwa | Migrated to Compose, connected to shared network |
| security-dashboard | Migrated to Compose |
| Conventions documented | https://wiki.veripath.co.uk/en/development/projects/compose-conventions |
Status: DONE 2026-08-16 — superseded by the 2026-09-11 re-arrangement (see the
status block at the top of this page and VPS Re-Arrangement 3).
The 3-VPS split was restored on 2026-08-16
(apps back on VPS2, ERPNext on VPS3, tunnels + bring-up.sh + this page updated).
Root cause of the 08-14 disappearance was established (deliberate
docker compose down+ prune from VPS1-origin SSH — see status block above).
The checklist below is retained as the historical record of what was executed.
Goal: booking apps (gp + dental) back on VPS2 (212.227.199.114); VPS1 keeps
gateway + data + AI; VPS3 keeps DefectDojo + DR. Restore documented resilience
(dedicated cores, no single point of failure).
Checklist (in order):
/var/lib/docker/containers/ last modified 2026-08-14 16:38; containers gone,gp_booking_app_client_postgres_data 71MB,dental_booking_app_orthanc-data 668K). Do NOT trust VPS2 until we know why./root/work/gp_booking_app + /root/work/dental_booking_app,.env/env.production presence.docker compose up -d --no-build for both repos)dsp_clinic_dev back on VPS2 client_vps_postgrestest-client id 3, port 172.18.0.1:5435), orclient_vps_postgres on VPS1).client_vps_postgres compose + tenant db_host/db_port to match.tun-apps-vps2.service (VPS1 nginx → VPS2 apps 8000/8010)172.18.0.1:5433) + tenant DBs172.18.0.1:5436/5437); re-point nginx gp.veripath.co.uk → VPS2 :8000 anddental.veripath.co.uk → VPS2 :8010 (remove the 8011 demo override).bring-up.sh (add VPS2 step), /root/AGENTS.md,/opt/check-tunnel-health.py), backups green.Notes / constraints:
bring-up.sh + infra-bringup.service handle all three hosts (fixed 2026-09-11) and the/root/work/veripath-ops.docker compose down VPS1's stacks until VPS2 is verified serving (keep a