~/docs / deployments
Deployments
Connect a GitHub repo (or paste any public URL) and Mercy takes care of everything: cloning, building inside Docker, streaming logs to your browser, uploading the build output to Cloudflare R2, and serving it instantly on a subdomain with automatic HTTPS.
Overview
Mercy deployments are designed for frontend projects — static sites, SPAs, and any framework that produces an output directory. Every deployment gets:
- ›An isolated Docker build environment — your repo never touches shared infra.
- ›Real-time build log streaming via WebSocket, replayed from the database on reconnect.
- ›Assets stored in Cloudflare R2 — globally distributed, low-latency delivery.
- ›A unique subdomain (e.g. my-app.yourdomain.com) provisioned automatically.
- ›Support for per-deployment environment variables, AES-encrypted at rest.
- ›Custom domain attachment with CNAME verification and on-demand TLS via Caddy.
How it works
From a POST request to a live URL, here is the full journey:
- 01
Job enqueued
The API receives your deploy request, creates a Project record in Postgres, and pushes a job onto the deploy-jobs BullMQ queue in Redis.
- 02
Clone & build
The Worker service picks up the job, clones the repository using simple-git, then runs your build command inside a Docker container with Node.js. stdout and stderr are piped line-by-line to the API's /internal/logs endpoint.
- 03
Log streaming
Each log line is persisted to the BuildLog table in Postgres and published to a Redis pub/sub channel (build:<projectId>). The WebSocket service is subscribed to that channel and forwards every line to all connected browser clients in real time.
- 04
Upload & serve
Once the build succeeds, the Worker recursively uploads the output directory to Cloudflare R2 under a project-specific prefix. The API updates the Project status to DEPLOYED and sets the subdomain. Subsequent requests to <subdomain>.yourdomain.com are proxied by the API to R2.
Quickstart
The fastest way to deploy is through the dashboard at /dashboard. Or use the API directly:
1. Authenticate
curl -s -X POST https://api.yourdomain.com/auth/login \
-H 'content-type: application/json' \
-d '{"email":"you@example.com","password":"yourpassword"}' \
| jq -r .token2. Create a deployment
TOKEN="<your-jwt>"
curl -X POST https://api.yourdomain.com/deploy \
-H "Authorization: Bearer $TOKEN" \
-H "content-type: application/json" \
-d '{
"repoUrl": "https://github.com/your-org/your-site",
"projectName": "my-site"
}'Response includes id (projectId) and initial status QUEUED.
3. Poll status
PROJECT_ID="<project-id>"
curl -s -H "Authorization: Bearer $TOKEN" \
https://api.yourdomain.com/deploy/$PROJECT_ID \
| jq .project.status
# "QUEUED" → "CLONING" → "BUILDING" → "DEPLOYED"4. Add environment variables (optional)
curl -X POST https://api.yourdomain.com/deploy/$PROJECT_ID/envvars \
-H "Authorization: Bearer $TOKEN" \
-H "content-type: application/json" \
-d '{"key": "NEXT_PUBLIC_API_URL", "value": "https://api.yourdomain.com"}'Env var values are AES-encrypted before storage. They are injected into the Docker build environment at build time.
Environment variables reference
These are the server-side variables required to run the deployment pipeline. Set them in apps/api/.env and apps/worker/.env.
| Variable | Service | Required | Description |
|---|---|---|---|
DATABASE_URL | api, worker | yes | PostgreSQL connection string |
JWT_SECRET | api | yes | JWT signing key, 32+ chars |
WORKER_SECRET | api, worker | yes | Shared secret for internal API routes |
REDIS_HOST | api, worker | yes | Redis hostname |
REDIS_PORT | api, worker | no | Redis port (default 6379) |
R2_ENDPOINT | api, worker | yes | Cloudflare R2 S3-compatible endpoint |
R2_ACCESS_KEY_ID | api, worker | yes | R2 access key |
R2_SECRET_ACCESS_KEY | api, worker | yes | R2 secret key |
R2_BUCKET_NAME | api, worker | yes | R2 bucket name |
ENV_ENCRYPTION_KEY | api | yes | 64-char hex key for env var encryption |
BASE_DOMAIN | api | no | Apex domain for subdomain routing (default localhost) |
GITHUB_CLIENT_ID | api | no | GitHub OAuth app client ID |
GITHUB_CLIENT_SECRET | api | no | GitHub OAuth app client secret |
API_BASE_URL | worker | no | API base URL (default http://localhost:3001) |
Custom domains
Every deployment gets a <subdomain>.<BASE_DOMAIN> URL automatically. You can also attach your own domain.
- 01
Add the domain
POST /deploy/:projectId/domains with { "domain": "app.yoursite.com" }. Mercy creates a CustomDomain record and returns the expected CNAME target.
- 02
Point your DNS
Create a CNAME record from app.yoursite.com → your BASE_DOMAIN. DNS propagation usually takes under 5 minutes.
- 03
TLS provisioned automatically
Caddy's on-demand TLS calls GET /internal/domain-check?domain=app.yoursite.com. Once the domain is verified in Postgres, Caddy issues a Let's Encrypt certificate on the first request.
Build logs
The dashboard opens a WebSocket connection to the WS service (port 3002) and displays logs in real time. The connection is authenticated with the same JWT used for REST requests.
- ›Live streaming — every Docker stdout/stderr line appears in the browser as it happens.
- ›Persistent replay — if you open the log panel after a build started, historical lines are loaded from Postgres then the live stream resumes seamlessly.
- ›Multi-client — multiple browser tabs watching the same project all receive the same stream via Redis pub/sub.
- ›stderr highlighting — lines from stderr are visually differentiated for quick error spotting.
- ›Status updates — project status transitions (QUEUED → CLONING → BUILDING → DEPLOYED or FAILED) are pushed over the same connection.
Set NEXT_PUBLIC_WS_URL in apps/web/.env.local to point the frontend at your WS service.