API Deployment Overview
The Sema Link API (semalink-api) runs as a Dockerised Node.js application on a DigitalOcean droplet. Unlike the frontend, which is a static build uploaded to Cloudflare Pages, the API is a long-running server process that requires a real VM.
Environments
| Environment | API Domain | Branch | Droplet |
|---|---|---|---|
| Staging | staging-arc.semalink.africa | staging | 209.38.197.79 (Frankfurt) |
| Test | test-arc.semalink.africa | test | TBD |
| Production | arc.semalink.africa | prod | TBD |
All arc subdomains are proxied through Cloudflare (orange cloud) and protected by Cloudflare Zero Trust Access. Direct requests to the origin IP without a valid service token are rejected at the edge.
Stack Summary
| Layer | Technology | Notes |
|---|---|---|
| Runtime | Node.js 22 (Alpine) | CommonJS output from TypeScript |
| Framework | Fastify 5 | |
| Reverse proxy | Caddy 2 | Handles HTTP/HTTPS on ports 80/443 |
| Message queue | RabbitMQ 3.13 | Runs as a local Docker container |
| Database | Neon (serverless Postgres) | External — Frankfurt region, staging branch |
| Cache | Upstash Redis | External — Frankfurt region, TLS (rediss://) |
| Containerisation | Docker Compose | Single docker-compose.staging.yml |
| CI/CD | GitHub Actions | SSH deploy triggered on push to staging |
How Secrets Are Managed
All environment variables are stored as GitHub Environment secrets under the staging environment in the semalink-api repository. On every deploy, the CI/CD workflow SSHes into the droplet and writes a fresh .env file from those secrets before restarting containers. The droplet itself holds no persistent secret state — it is always overwritten on deploy.
Service Architecture on the Droplet
Three Docker containers run in a single Compose project:
Internet (HTTPS)
↓
Cloudflare Edge (TLS termination + Zero Trust)
↓ HTTP/HTTPS
Caddy :80 / :443
↓ HTTP
API (port 3000, internal only)
↓ AMQP
RabbitMQ :5672- Caddy is the only container exposed to the host network on ports 80 and 443. It proxies all traffic to the
apicontainer on port 3000 using Docker's internal DNS (api:3000). Port 3000 is not bound to the host. - RabbitMQ is exposed on port 5672 (AMQP) and 15672 (management UI) for local debugging.
- PostgreSQL and Redis are fully external — no containers for them.
Further Reading
- Staging Infrastructure Setup — one-time droplet provisioning walkthrough
- API CI/CD Pipeline — how GitHub Actions deploys to the droplet
- Environment Variables — full variable reference
- Zero Trust & API Access — Cloudflare Access configuration