VPS Build Plan — EduNimbus Production
Live infrastructure at 167.233.208.4 · Coolify v4.1.2 · Docker Compose on Traefik · Cloudflare DNS
Infrastructure is live
Coolify is running at coolify.edunimbus.co.za· Projects created: “QCTO Submissions” and “SDP LMS System” · Traefik handles SSL for all *.edunimbus.co.za subdomains automatically.
Two-Project Architecture
QCTO Submissions — QCTOCloud
Admin-facing compliance engine. Sold to SDPs for QCTO workflow management & NLRD submission.
qctocloud.<sdp>.co.za
dashboard.edunimbus.co.za (demo)
Stack: Next.js 15 + PostgreSQL 16 + Drizzle
SDP LMS System — EduNimbus LMS
Learner-facing LMS. Up-sell to P1 clients. Offline-first PWA for load shedding resilience.
learner.<sdp>.co.za
lms-sdp.edunimbus.co.za (demo)
Stack: Nuxt 3 PWA + Directus 11 + PostgreSQL 16
POPIA architecture:Each college runs on a dedicated VPS with isolated Docker volumes. Cross-tenant data leakage is architecturally impossible. Learner PII never leaves the college's own server. This is the core sales argument against cloud-hosted competitors.
Live DNS Records — edunimbus.co.zaCloudflare · NS: becky + theo
| Record | Target | Proxied | Purpose |
|---|---|---|---|
| coolify.edunimbus.co.za | 167.233.208.4 | → Direct | Coolify panel — must be unproxied (direct) |
| dashboard.edunimbus.co.za | 167.233.208.4 | ☁ Proxied | QCTOCloud demo/sales instance |
| api-qcto.edunimbus.co.za | 167.233.208.4 | → Direct | QCTOCloud API — unproxied for TLS passthrough |
| api-submissions.edunimbus.co.za | 167.233.208.4 | → Direct | QCTO MIS submission relay (future) |
| api-eisa.edunimbus.co.za | 167.233.208.4 | → Direct | EISA integration endpoint (future) |
| lms-sdp.edunimbus.co.za | 167.233.208.4 | ☁ Proxied | LMS demo instance (Project 2) |
| api-lms.edunimbus.co.za | 167.233.208.4 | ☁ Proxied | LMS Directus API (Project 2) |
| edunimbus.co.za | 167.233.208.4 | ☁ Proxied | Marketing site (apex) |
Important: coolify.edunimbus.co.za must stay unproxied (DNS only)in Cloudflare. Coolify uses WebSockets for real-time deployments — Cloudflare's proxy breaks these. API endpoints that need direct TLS passthrough should also be unproxied.
In Coolify → Projects → QCTO Submissions → + Add Resource → Docker Compose (Empty). Paste the compose below. Set all environment variables in Coolify's env panel — never in the compose file.
Go to Coolify → Projects
QCTO Submissions → + Add Resource
Docker Compose (Empty) → paste below
# QCTOCloud — Coolify Docker Compose Resource
# Add via Coolify: Project → QCTO Submissions → + Add Resource → Docker Compose
# Coolify Traefik handles SSL automatically — no Nginx/Caddy needed
# Set all env vars in Coolify → Resource → Environment Variables (not in this file)
version: "3.9"
services:
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
POSTGRES_DB: ${DB_NAME:-qctocloud}
POSTGRES_USER: ${DB_USER:-qcto}
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- pgdata:/var/lib/postgresql/data
networks: [qctocloud_net]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-qcto} -d ${DB_NAME:-qctocloud}"]
interval: 10s
timeout: 5s
retries: 5
expose: ["5432"] # internal only — never expose DB port to host
app:
image: ghcr.io/edunimbus/qctocloud:${IMAGE_TAG:-latest}
restart: unless-stopped
environment:
NODE_ENV: production
DATABASE_URL: postgres://${DB_USER:-qcto}:${DB_PASSWORD}@db:5432/${DB_NAME:-qctocloud}
NEXTAUTH_URL: https://${APP_DOMAIN}
NEXTAUTH_SECRET: ${NEXTAUTH_SECRET}
depends_on:
db:
condition: service_healthy
networks: [qctocloud_net]
expose: ["13000"]
# Coolify Traefik labels (auto-added by Coolify — listed here for reference)
labels:
- "traefik.enable=true"
- "traefik.http.routers.qctocloud-${TENANT_SLUG}.rule=Host(`${APP_DOMAIN}`)"
- "traefik.http.routers.qctocloud-${TENANT_SLUG}.tls=true"
- "traefik.http.routers.qctocloud-${TENANT_SLUG}.tls.certresolver=letsencrypt"
- "traefik.http.services.qctocloud-${TENANT_SLUG}.loadbalancer.server.port=13000"
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:13000/api/health || exit 1"]
interval: 30s
timeout: 10s
retries: 3
start_period: 20s
volumes:
pgdata:
name: qctocloud_${TENANT_SLUG}_pgdata
networks:
qctocloud_net:
name: qctocloud_${TENANT_SLUG}_net
driver: bridgeTenant Resource Naming Convention
| TENANT_SLUG | App Domain | DB Volume Name | Status |
|---|---|---|---|
| edunimbus-demo | dashboard.edunimbus.co.za | qctocloud_edunimbus-demo_pgdata | provision |
| mzansi-training | qctocloud.mzansitraining.co.za | qctocloud_mzansi-training_pgdata | template |
| cape-skills | qctocloud.capeskills.co.za | qctocloud_cape-skills_pgdata | template |
| <new-tenant> | qctocloud.<sdp>.co.za | qctocloud_<slug>_pgdata | template |
Volume names are deterministic: qctocloud_{TENANT_SLUG}_pgdata. Never delete a named volume without a verified backup. Coolify labels containers with the resource name automatically.
Project 2 — SDP LMS System (awareness)
The LMS (learner.<sdp>.co.za) is a separate up-sell product using Nuxt 3 PWA + Directus 11. It uses the same VPS and Coolify project “SDP LMS System” but is a completely independent stack. Engineering build plan is documented. Build begins after QCTOCloud auth is shipped.
Backend
Directus 11
Frontend
Nuxt 3 PWA
Offline
Dexie.js IndexedDB