~/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:

  1. 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.

  2. 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.

  3. 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.

  4. 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

bash
curl -s -X POST https://api.yourdomain.com/auth/login \
  -H 'content-type: application/json' \
  -d '{"email":"you@example.com","password":"yourpassword"}' \
  | jq -r .token

2. Create a deployment

bash
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

bash
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)

bash
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.

VariableServiceRequiredDescription
DATABASE_URLapi, workeryesPostgreSQL connection string
JWT_SECRETapiyesJWT signing key, 32+ chars
WORKER_SECRETapi, workeryesShared secret for internal API routes
REDIS_HOSTapi, workeryesRedis hostname
REDIS_PORTapi, workernoRedis port (default 6379)
R2_ENDPOINTapi, workeryesCloudflare R2 S3-compatible endpoint
R2_ACCESS_KEY_IDapi, workeryesR2 access key
R2_SECRET_ACCESS_KEYapi, workeryesR2 secret key
R2_BUCKET_NAMEapi, workeryesR2 bucket name
ENV_ENCRYPTION_KEYapiyes64-char hex key for env var encryption
BASE_DOMAINapinoApex domain for subdomain routing (default localhost)
GITHUB_CLIENT_IDapinoGitHub OAuth app client ID
GITHUB_CLIENT_SECRETapinoGitHub OAuth app client secret
API_BASE_URLworkernoAPI base URL (default http://localhost:3001)

Custom domains

Every deployment gets a <subdomain>.<BASE_DOMAIN> URL automatically. You can also attach your own domain.

  1. 01

    Add the domain

    POST /deploy/:projectId/domains with { "domain": "app.yoursite.com" }. Mercy creates a CustomDomain record and returns the expected CNAME target.

  2. 02

    Point your DNS

    Create a CNAME record from app.yoursite.com → your BASE_DOMAIN. DNS propagation usually takes under 5 minutes.

  3. 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.