NixOS deployment for Catapult Central and Worker.
- NixOS 24.05+
- PostgreSQL (Central)
- Podman + Caddy (Worker)
- GitHub App configured
- Create GitHub App at Settings → Developer settings → GitHub Apps
- Configure:
- Webhook URL:
https://catapult.example.com/webhook/github - Permissions: Contents (Read), Pull requests (Read & Write)
- Events: Pull request, Push
- Webhook URL:
- Generate and download the private key
# Generate shared secret (same on Central and Worker)
openssl rand -base64 32 > /var/lib/catapult/worker-secret
# GitHub webhook secret
echo "your-webhook-secret" > /var/lib/catapult/webhook-secret
# GitHub App private key
cp downloaded-key.pem /var/lib/catapult/github-private-key.pem
chmod 600 /var/lib/catapult/*{ config, pkgs, ... }:
{
imports = [ /path/to/catapult/nixos/catapult.nix ];
services.postgresql = {
enable = true;
ensureDatabases = [ "catapult" ];
ensureUsers = [{ name = "catapult"; ensureDBOwnership = true; }];
};
services.catapult.central = {
enable = true;
databaseUrl = "postgresql://catapult@localhost/catapult";
githubAppId = 123456;
githubPrivateKeyFile = "/var/lib/catapult/github-private-key.pem";
githubWebhookSecretFile = "/var/lib/catapult/webhook-secret";
workerSharedSecretFile = "/var/lib/catapult/worker-secret";
# Workers by zone (tenant)
workers = {
acme-corp = "https://deployer.acme.example.com";
contoso = "https://deployer.contoso.example.com";
};
};
services.caddy.virtualHosts."catapult.example.com".extraConfig = ''
reverse_proxy localhost:8080
'';
}{ config, pkgs, ... }:
{
imports = [
/path/to/catapult/nixos/catapult.nix
/path/to/catapult/nixos/podman.nix
];
services.catapult-podman = {
enable = true;
user = "catapult-worker";
};
services.catapult.worker = {
enable = true;
centralUrl = "https://catapult.example.com";
workerSharedSecretFile = "/var/lib/catapult/worker-secret";
caddyAdminApi = "http://localhost:2019";
sitesDir = "/var/www/sites";
# Optional: Cloudflare Tunnel for DNS management
cloudflare = {
enable = true;
apiTokenFile = "/var/lib/catapult/cloudflare-token";
accountId = "your-account-id";
tunnelId = "your-tunnel-id";
serviceUrl = "http://localhost:8080";
};
};
services.caddy = {
enable = true;
globalConfig = "admin localhost:2019";
virtualHosts."example.com".extraConfig = ''
# Static config - Catapult adds dynamic routes via admin API
'';
virtualHosts."*.example.com".extraConfig = ''
# Wildcard for PR previews
'';
};
}{
"zone": "acme-corp",
"domain_pattern": "{repo}.example.com",
"pr_pattern": "pr-{pr}-{repo}.example.com"
}{
"build_type": "sveltekit",
"build_command": "npm run build",
"output_dir": "build"
}| Field | Description | Example |
|---|---|---|
zone |
Worker zone | "acme-corp" |
domain_pattern |
Main branch domain | "{repo}.example.com" |
pr_pattern |
PR preview domain | "pr-{pr}-{repo}.example.com" |
domain |
Explicit domain | "example.com" |
subdomain |
Subdomain prefix | "www" |
build_type |
sveltekit, vite, zola, custom |
"sveltekit" |
build_command |
Custom build command | "npm run build" |
output_dir |
Output directory | "build" |
For automatic DNS record and tunnel ingress management:
- Create tunnel in Cloudflare Zero Trust (remotely managed)
- Create API token with DNS:Edit and Cloudflare Tunnel:Edit permissions
- Configure
services.catapult.worker.cloudflarewith IDs
On deploy, Catapult creates:
- Tunnel ingress rule:
pr-42-website.example.com → http://localhost:8080 - DNS CNAME:
pr-42-website.example.com → {tunnel-id}.cfargotunnel.com
# Central
systemctl status catapult-central
journalctl -u catapult-central -f
curl http://localhost:8080/health
# Worker
systemctl status catapult-worker
journalctl -u catapult-worker -fWebhook signature invalid: Check webhook secret matches GitHub App config
Podman connection failed:
ls -la /run/podman/podman.sock
systemctl start podman.socketCaddy route fails:
curl http://localhost:2019/config/