Docs / Install
DNS and TLS
Wildcard DNS records, ACME DNS-01 wildcard certificates, BYO certs.
Every preview gets a subdomain. The wildcard cert path lets a fresh PR land on HTTPS within seconds of the build finishing — no per-PR cert issuance, no rate-limit risk.
DNS records you need
Two A/AAAA records pointing at the host that runs the ingress proxy:
| Record | Purpose |
|---|---|
galley.yourco.dev | The dashboard / API. Matches GALLEY_PUBLIC_HOST. |
*.preview.yourco.dev | Every preview lands under this wildcard. Matches GALLEY_PREVIEW_DOMAIN. |
Single host: both records point at the same IP. Split topology: both records point at whichever host runs the ingress proxy (usually the control-plane host).
If your zone provider doesn’t allow wildcards directly, use a DNS provider that does (Route 53, Cloudflare, deSEC, etc.) or set the wildcard via API.
ACME DNS-01 (recommended)
Galley uses the DNS-01 challenge, so wildcard issuance works without exposing any service publicly during the handshake. You give it credentials for your DNS provider, it writes a TXT record, gets a wildcard cert, and removes the TXT.
Set GALLEY_LE_DNS_PROVIDER plus the provider’s expected env vars:
| Provider | GALLEY_LE_DNS_PROVIDER | Required env |
|---|---|---|
| Cloudflare | cloudflare | CLOUDFLARE_DNS_API_TOKEN (scoped to Zone:Read + DNS:Edit on the zone) |
| Route 53 | route53 | AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGION |
| Google Cloud DNS | gcloud | GCE_PROJECT, GCE_SERVICE_ACCOUNT_FILE (mounted into the proxy container) |
| Azure DNS | azuredns | AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID, AZURE_SUBSCRIPTION_ID, AZURE_RESOURCE_GROUP |
| DigitalOcean | digitalocean | DO_AUTH_TOKEN |
| Hetzner | hetzner | HETZNER_API_KEY |
| deSEC | desec | DESEC_TOKEN |
| Linode | linode | LINODE_TOKEN |
| Gandi | gandi | GANDI_API_KEY |
Other providers are supported — the full list is documented at the ACME library used by the proxy. Set the matching env vars in .env; the proxy reads them at boot.
Cloudflare token scopes
For the API token, the minimum scopes are:
- Zone:
Read(so the lookup of your zone works) - DNS:
Edit(to write/delete the_acme-challengeTXT)
Restrict the token to the specific zone — don’t issue an account-wide token unless you have a reason.
Bring-your-own cert (BYO)
If you can’t use ACME (air-gapped, internal-only certs from your PKI, etc.), drop the cert + key into the proxy’s TLS volume and tell it to use them:
# In .env
GALLEY_TLS_MODE=byo
# Mount the cert + key
- ./tls/cert.pem:/etc/ssl/cert.pem:ro
- ./tls/key.pem:/etc/ssl/key.pem:ro
The cert needs to be a wildcard for *.preview.yourco.dev (and ideally also cover the dashboard host). You’re responsible for renewal — Galley won’t rotate a BYO cert on its own.
Internal-only previews
If previews shouldn’t be publicly reachable at all (corporate network, VPN-only review), keep DNS internal:
- Wildcard pointing at an internal IP, resolvable only from your VPN / private DNS.
- The proxy exposes
:80/:443on the internal interface. - Pair with
preview-accessset toip_allowlistfor a second layer.
This is the standard config for teams where reviewers connect via Tailscale, ZeroTier, or a corporate VPN. ACME DNS-01 still works because the challenge happens through your DNS provider’s API, not by exposing a port.
Troubleshooting
- Cert still pending after several minutes: check
docker logs <proxy container>for ACME errors. Most failures are credential scope (token can’t write the TXT) or a registrar that’s slow to propagate. dns_problem: the challenge TXT was written but Let’s Encrypt couldn’t read it back yet. Some registrars take 60–120s to propagate; the resolver retries automatically.- Wrong cert showing: make sure both the dashboard host and the wildcard resolve to the same proxy. A mismatched DNS view (the proxy gets one cert request but a different one is in cache) usually means a stale
acme.json. Stop the proxy, delete the volume, restart.