Preview deployments
Give every pull request, branch or commit its own URL with one API call, and remove it when you're done.
Each preview is a container in the ceryx network plus a route to it. In this guide, pull request 121 is served at pr-121.preview.example.com by a container named preview-pr-121.
1. Point a wildcard DNS record at the server
Create one DNS record for every preview at once:
*.preview.example.com. A 203.0.113.10
2. Store a wildcard certificate
Without one, Ceryx gets a Let's Encrypt certificate for each preview the first time it's visited. That works for a few previews, but Let's Encrypt limits how many certificates a domain can get each week.
A wildcard certificate for *.preview.example.com covers every preview instead. Get one with a DNS challenge, for example with Certbot or lego and your DNS provider, then store it in Ceryx:
jq -n \
--rawfile certificate fullchain.pem \
--rawfile private_key privkey.pem \
'{domain: "*.preview.example.com", certificate: $certificate, private_key: $private_key}' |
docker compose -p ceryx exec -T ceryx curl -X POST localhost:8080/api/certificates \
-H 'Content-Type: application/json' \
-d @-
Store the renewed certificate the same way before this one expires.
3. Deploy a preview
Save this script on the server as deploy-preview.sh. It replaces any previous container for the same preview, then creates or replaces its route:
#!/usr/bin/env bash
# Usage: deploy-preview.sh <id> <image>, e.g. deploy-preview.sh pr-121 ghcr.io/example/app:pr-121
set -euo pipefail
id="$1"
image="$2"
docker rm -f "preview-$id" 2>/dev/null || true
docker run -d --name "preview-$id" --network ceryx --restart unless-stopped "$image"
docker compose -p ceryx exec -T ceryx curl -fsS -X POST localhost:8080/api/routes \
-H 'Content-Type: application/json' \
-d "{\"source\": \"$id.preview.example.com\", \"target\": \"http://preview-$id:8000\"}"
The preview is live as soon as the script finishes.
4. Remove a preview
Save this as remove-preview.sh. Deleting a route that doesn't exist also succeeds, so it's safe to run more than once:
#!/usr/bin/env bash
# Usage: remove-preview.sh <id>
set -euo pipefail
id="$1"
docker compose -p ceryx exec -T ceryx curl -fsS -X DELETE "localhost:8080/api/routes/$id.preview.example.com"
docker rm -f "preview-$id" 2>/dev/null || true
5. Run it from CI
Install a self-hosted GitHub Actions runner on the server that runs Ceryx, and make the scripts available to it. The runner needs Docker access and permission to call the Ceryx API. Then call the scripts directly when a pull request opens, updates or closes:
on:
pull_request:
types: [opened, synchronize, reopened, closed]
jobs:
preview:
runs-on: self-hosted
steps:
- name: Deploy preview
if: github.event.action != 'closed'
run: ./deploy-preview.sh "pr-${{ github.event.number }}" "ghcr.io/example/app:pr-${{ github.event.number }}"
- name: Remove preview
if: github.event.action == 'closed'
run: ./remove-preview.sh "pr-${{ github.event.number }}"
This assumes an earlier job builds and pushes the image for the pull request. Since the runner has Docker access to your server, only run these workflows for pull requests from trusted contributors.
Previews for branches and commits
Use any id that is a valid DNS label: lowercase letters, digits and hyphens, up to 63 characters. For example main for a branch, or sha-3f9c2a1 for a commit.