Deploy System
Technical deep-dive into Dictumal's production MVP streaming deployment path.
Current Architecture
Every deployment creates one DigitalOcean Ubuntu droplet and configures a browser-streamed Linux desktop via cloud-init.
- Cloud provider: DigitalOcean Droplets via
src/lib/deploy/digitalocean.ts. - Bootstrap:
src/lib/deploy/cloud-init.tsinstalls XFCE, TigerVNC, Docker, Guacamole, Nginx, and UFW. - Desktop runtime: TigerVNC on display
:1(TCP5901), plus XFCE startup service. - Gateway: Guacamole containers (
guacd+guacamole) with JSON-auth token launch. - HTTPS: Nginx reverse proxy on
443with a self-signed certificate and HTTP->HTTPS redirect. - Firewall: UFW allows
22,80,443; everything else is denied.
End-To-End Request Flow
- User starts deployment from the constitution UI (
src/components/constitution/deploy-dialog.tsx). - Frontend calls
POST /api/deploymentswith{ constitutionId, region?, size? }. - API generates an 8-char VNC password, derives the Guacamole JSON key, generates cloud-init, and calls DO droplet create.
- Deployment row is persisted in Prisma with status
PROVISIONING. - Client polls
GET /api/deployments/[id]every 5s. - Status route transitions through
PROVISIONING->CONFIGURING->ACTIVEbased on droplet state and readiness probes. - Active launch uses
GET /api/deployments/[id]/launch(or?target=ip) which returns a302redirect to Guacamole with encrypted short-lived token data.
Deployment State Machine
PROVISIONING -> CONFIGURING -> ACTIVE
PROVISIONING/CONFIGURING -> ERROR
Any status -> DESTROYED (on DELETE)Readiness Probes During CONFIGURING
GET /api/deployments/[id] computes progress from these checks against deployment IP:
- TCP
5901(VNC socket) - HTTP
http://[ip]/ - HTTP
http://[ip]:8080/guacamole/ - HTTPS
https://[ip]/guacamole/with insecure cert probe
Deployment is promoted to ACTIVE only when HTTPS Guacamole is reachable. A single false VNC check does not block activation.
Required Environment Variables
| Variable | Required | Used By | Behavior |
|---|---|---|---|
DIGITALOCEAN_API_TOKEN | Yes (for deploy) | src/lib/deploy/digitalocean.ts | Authenticates DO API calls for create/get/delete droplet. |
DIGITALOCEAN_IMAGE_SLUG | No | src/lib/deploy/digitalocean.ts | Explicit image override. If set, latest-image discovery is skipped. |
DIGITALOCEAN_USE_LATEST_UBUNTU_IMAGE | No | src/lib/deploy/digitalocean.ts | If truthy, discovers newest Ubuntu image slug from DO catalog; otherwise defaults to ubuntu-24-04-x64. |
DIGITALOCEAN_SSH_KEYS | No | src/lib/deploy/digitalocean.ts | Comma-separated SSH key IDs/fingerprints attached to droplets. |
NEXT_PUBLIC_STREAM_GATEWAY_HOST | No | /api/deployments/[id]/launch, deployment UI, instances UI | Preferred hostname for launch redirects. Defaults to stream.the-next-lab.com; falls back to IP when blank or when ?target=ip is used. |
Provisioning Timeline And User Statuses
Typical timeline (varies by region/image cache/network):
- 0-2 min:
PROVISIONING(DigitalOcean droplet creation). - 2-10 min:
CONFIGURING(cloud-init package install + container startup + HTTPS gateway setup). - Ready:
ACTIVEonce HTTPS Guacamole responds.
Timeout rule: if CONFIGURING exceeds 20 minutes, API marks deployment ERROR and stores an error message pointing to /var/log/dictumal-init.log.
Progress stages emitted by GET /api/deployments/[id]:
| Stage | Percent | When Emitted |
|---|---|---|
provisioning-droplet | 22% | Waiting for provider droplet activation. |
waiting-for-ip | 35% | Droplet active but public IP missing. |
installing-packages | 48% | No service probes are up yet. |
starting-guacamole | 62% | VNC socket is reachable. |
configuring-https | 78% | Guacamole HTTP endpoint (:8080) responds. |
final-health-check | 92% | Guacamole HTTPS endpoint responds. |
active | 100% | Status promoted to ACTIVE. |
UI behavior: deployment dialogs and instance progress components poll status every 5 seconds, so state/button changes can appear one poll cycle after backend state has changed.
Troubleshooting Playbook
1) Auth Configuration Failures
Symptoms:
- Deployment endpoints return
401 Unauthorized. - Launch endpoint returns not found for a deployment the user expects to own.
Checks:
- Confirm session exists (re-login).
- Verify auth env vars are correct for current host (
AUTH_URL/NEXTAUTH_URL,AUTH_SECRET/NEXTAUTH_SECRET, Google OAuth credentials). - Confirm OAuth callback URL matches the running environment.
2) VNC Connectivity Looks Broken
Symptoms:
- Progress check shows
vnc5901: false. - Deployment still advances to
configuring-httpsorACTIVE.
Interpretation and checks:
- This can be expected. Port
5901is not a hard gate to activation. - Validate stronger checks first:
guacHttp8080andguacHttps443. - On droplet, inspect
/var/log/dictumal-init.log,systemctl status vncserver vnc-xstartup nginx, and Docker logs for Guacamole containers.
3) ACTIVE Button Refresh Feels Delayed
Symptoms:
- Backend status is already
ACTIVE. - UI still shows deploying state or active actions appear a few seconds later.
Why this happens:
- Status polling interval is 5 seconds.
- Server-rendered sections (like instances cards) update after client polling detects terminal state and triggers refresh.
Operator action: wait one extra poll cycle and re-open the deploy dialog or refresh the instances page if needed.
Security Notes And MVP Limitations
- TLS is currently self-signed per droplet; clients may show certificate warnings.
- Launch tokens are short-lived (5 minutes) but are URL query params, so treat redirected URLs as sensitive.
- VNC password is stored with deployment metadata and used to derive Guacamole JSON key in current MVP design.
- Cloud-init creates a default
dictumaluser/password pair for bootstrap convenience; hardening and credential rotation are pending. - Browser-stream readiness is the MVP success condition; deeper policy enforcement and stronger runtime hardening are deferred.