Caret’s production environment is designed to be simple and cost-effective without sacrificing reliability. The entire backend stack runs on a single Hetzner Cloud VPS (roughly 9 EUR/month), managed by Coolify — a self-hosted open-source PaaS that handles deployments, domain bindings, TLS certificates, and container restarts. Docker Compose is the deployment unit, Cloudflare manages DNS for theDocumentation Index
Fetch the complete documentation index at: https://mintlify.com/arrozet/caret/llms.txt
Use this file to discover all available pages before exploring further.
caret.page domain, and Supabase Cloud provides managed PostgreSQL, authentication, and vector search outside the VPS. This setup gives a PaaS-grade developer experience at a fraction of the cost of AWS or managed Kubernetes.
Infrastructure Stack
| Layer | Choice | Notes |
|---|---|---|
| Hosting | Hetzner Cloud VPS | Single VM, ~9 EUR/month; sufficient for the full stack |
| PaaS | Coolify (self-hosted) | Deployments, domains, TLS, logs, redeploys, and Docker Compose resources |
| Containers | Docker Compose (docker-compose.prod.yml) | Production service topology |
| CI/CD | GitHub Actions + Coolify | Automatic deploys from the prod branch |
| DNS | Cloudflare | Manages caret.page and all subdomains |
| Database / Auth | Supabase Cloud | Managed PostgreSQL, Auth, and pgvector — external to the VPS |
Production Domains
All public traffic enters through Coolify’s built-in reverse proxy, which terminates TLS and routes requests to the appropriate container. No internal service ports are exposed directly to the internet.| Domain | Service | Notes |
|---|---|---|
caret.page | Frontend | Main user-facing app |
www.caret.page | Frontend alias | CNAME to caret.page |
api.caret.page | API Gateway | Public REST API entrypoint for all /api/v1/ traffic |
ws.caret.page | Collab Service | Public WebSocket endpoint — bypasses the API Gateway |
ops.caret.page | Coolify dashboard | PaaS control plane for deployments and operations |
Setup Guide
Create a new VPS on Hetzner Cloud (CX21 or higher recommended). SSH into the server and install Coolify using its official one-line installer:
Once installation finishes, Coolify’s dashboard is available at your server’s public IP on port
8000. Immediately secure it by setting up a domain (ops.caret.page) and enabling HTTPS. Do not expose port 8000 in the Hetzner firewall — access the dashboard only through the ops.caret.page domain once DNS and TLS are configured.In the Cloudflare dashboard for
caret.page, create A records pointing each subdomain to the VPS public IPv4 address:@ (caret.page)<VPS IPv4>wwwcaret.pageapi<VPS IPv4>ws<VPS IPv4>ops<VPS IPv4>Use DNS-only (grey cloud) mode in Cloudflare unless you have a specific reason to enable Cloudflare proxying — Coolify handles TLS natively via ACME/Let’s Encrypt.
/docker-compose.prod.yml.prod.frontendhttps://caret.pageapi-gatewayhttps://api.caret.pagecollab-servicehttps://ws.caret.pagehttps://ops.caret.pageCoolify will automatically provision TLS certificates via Let’s Encrypt for each domain once DNS propagates.
In the Coolify environment variable editor for the project, add all required secrets. Never commit these values to the repository.
# Database — required by all backend services
DATABASE_URL=postgresql://postgres.[ref]:[password]@aws-0-[region].pooler.supabase.com:6543/postgres
# Supabase
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_ANON_KEY=your-anon-key
SUPABASE_SERVICE_ROLE_KEY=your-service-role-key # auth-service and document-service
SUPABASE_JWT_SECRET=your-jwt-secret # collab-service
# JWT signing secret shared across Node and AI services
JWT_SECRET=a-long-random-secret
# CORS — allow production origins only
ALLOWED_ORIGINS=https://caret.page,https://www.caret.page
# API Gateway rate limiting (optional — defaults shown)
RATE_LIMIT_MAX=1000
RATE_LIMIT_WINDOW_MINUTES=15
# Frontend build-time vars (baked in at image build time)
VITE_API_BASE_URL=https://api.caret.page/api/v1
VITE_API_URL=https://api.caret.page/api/v1
VITE_COLLABORATION_WS_URL=wss://ws.caret.page/document
VITE_COLLAB_WS_URL=wss://ws.caret.page/document
VITE_ENABLE_COLLABORATION=true
VITE_APP_ORIGIN=https://caret.page
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key
# AI provider keys — at least one required
OPENAI_API_KEY=sk-...
ANTHROPIC_API_KEY=sk-ant-...
OPENROUTER_API_KEY=sk-or-...
OPENROUTER_MODEL=deepseek/deepseek-v4-flash # default model if OPENROUTER_API_KEY is set
The production compose file (
docker-compose.prod.yml) uses :? syntax for required variables — for example DATABASE_URL:?DATABASE_URL is required. Docker Compose will refuse to start a service if a required variable is missing or empty, preventing silent misconfiguration.Trigger the first deployment from the Coolify dashboard. Once containers are running, verify each service is healthy by hitting its health endpoint:
Hetzner Firewall Rules
Configure the Hetzner Cloud firewall to allow only the required ingress. All other ports should be blocked by default.| Protocol | Port / Type | Purpose |
|---|---|---|
| TCP | 22 | SSH administration |
| TCP | 80 | HTTP (needed for ACME challenges) |
| TCP | 443 | HTTPS / WSS public traffic |
| ICMP | — | Basic reachability / ping |
Production Compose Differences
Thedocker-compose.prod.yml file differs from the development compose in several important ways:
- Services use
exposeinstead ofports— no ports are bound to the host; Coolify’s reverse proxy handles ingress. - All containers set
restart: unless-stoppedfor automatic recovery after crashes or reboots. - Health check intervals are
30s(vs10sin development) to reduce overhead. The AI servicestart_periodis20sin production (vs15sin development). - Required environment variables use
:?error syntax to fail fast on misconfiguration (e.g.DATABASE_URL:?DATABASE_URL is required). AI provider keys remain optional with empty-string defaults. - The frontend builds from
Dockerfile.prodwith production API URLs baked in at build time asARGvalues. In production, the frontend container serves on port8080(not5173as in development). - The API Gateway exposes additional rate-limiting configuration via
RATE_LIMIT_MAXandRATE_LIMIT_WINDOW_MINUTESenvironment variables.
Monitoring
Coolify Logs
View real-time container logs and historical output for every service from the Coolify dashboard under Services → Logs.
Sentry
Sentry is the error tracking solution for both frontend (React) and backend (Node/Python) services. Configure
SENTRY_DSN per service to capture exceptions and performance traces.Deprecated Deployment Targets
The following infrastructure options were evaluated and rejected during the architecture phase. They are documented here for historical context only. Do not attempt to deploy to these targets unless the architecture decision is explicitly revisited.
| Target | Service | Status |
|---|---|---|
| Vercel | Frontend | Considered but rejected — the Hetzner VPS has sufficient resources and keeps deployment centralised |
| AWS Lambda | API Gateway, Auth, Document | Rejected — AWS costs were too high for an academic-project budget |
| AWS ECS Fargate | Collab, AI Service | Rejected — always-on containers are cheaper and simpler on Hetzner |
| Supabase Cloud | PostgreSQL + Auth + pgvector | Still active — remains the managed database/auth/vector platform outside the VPS |