HTTPS and custom domains
How to enable TLS termination and configure the domain used for preview URLs.
PullPreview gives you two knobs for HTTPS and public URLs:
proxy_tls— enables HTTPS termination via Caddy and Let’s Encrypt.dns— controls the domain suffix used in generated preview hostnames.
Both are optional inputs in your workflow’s with: block.
TLS termination with proxy_tls
When proxy_tls is set, PullPreview puts a Caddy ↗ reverse proxy in front of your app — a sidecar container for compose, or a Caddy Deployment for helm. Caddy handles Let’s Encrypt certificate issuance and renewal, then forwards HTTPS traffic to the named service and port.
The format is <service>:<port> where service is the name of a service that listens on port (HTTP, not HTTPS — Caddy terminates TLS). The port is the internal port your service listens on, not a provider-specific value.
with: proxy_tls: web:80Caddy obtains a certificate from Let’s Encrypt for the preview hostname, so your app is accessible over HTTPS from the first deploy. Setting proxy_tls forces the preview URL to https://…:443 (the default_port input is ignored).
If you do not set proxy_tls, the preview URL uses plain HTTP.
Caveats (Compose)
- The target service and port must exist in the resolved Compose config, or the deploy fails.
- PullPreview injects a service named
pullpreview-proxy(imagecaddy:2-alpine, publishing443). If your stack already defines a service with that name, the deploy errors. - If your stack already publishes host port
443itself, PullPreview skips the Caddy injection (logging a warning) and uses your own443service.
Additional hostnames (Helm)
For helm previews, proxy_tls is required, and you can serve extra public hostnames from the same Caddy gateway with proxy_tls_hosts (comma-separated). Each one reverse-proxies to the same upstream Service and supports placeholders:
with: deployment_target: helm proxy_tls: "{{ release_name }}-web:80" proxy_tls_hosts: "admin.{{ pullpreview_public_dns }}"See Deployment targets for the full Helm story.
DNS suffix with dns
The dns input sets the domain suffix for the auto-generated preview hostname. The default is my.preview.run.
with: dns: my.preview.run # defaultHostnames embed the PR number, branch, and the instance’s public IP, and look like:
pr-<pr-number>-<branch>-ip-<dashed-public-ip>.<dns>For example, PR #15 on branch my-feature at IP 1.2.3.4 becomes pr-15-my-feature-ip-1-2-3-4.my.preview.run. (Long names are truncated to fit the max_domain_length limit.)
Built-in alternative domains
If you use proxy_tls with the default my.preview.run domain and you have high PR churn, you may hit Let’s Encrypt’s rate limits (certificates per registered domain per week). PullPreview provides nine built-in alternative domains, each on a separate registered domain so they have independent rate-limit quotas:
with: proxy_tls: web:80 dns: rev1.clickUse any of rev1.click through rev9.click. Spread load across multiple domains if you run many concurrent previews.
Custom domain
You can use your own domain. Because each preview’s IP is embedded in the hostname and changes per environment, a static wildcard A record won’t work — instead, delegate the subdomain to PullPreview’s resolver, which decodes the IP from the hostname.
Assuming you own example.com and want previews under *.preview.example.com, add an NS record in your DNS zone delegating preview.example.com to xip.preview.run, then set dns:
with: dns: preview.example.comPreview hostnames will then look like pr-15-my-feature-ip-1-2-3-4.preview.example.com. Using your own domain also gives you an independent Let’s Encrypt rate-limit quota.
Combining proxy_tls and dns
Both inputs work together. A typical setup for production-grade previews:
with: proxy_tls: web:80 dns: rev1.clickThis gives a Let’s Encrypt-issued certificate on a rate-limit-friendly domain. The preview URL posted to the PR will be https://pr-<pr>-<branch>-ip-<dashed-ip>.rev1.click.
Rate-limit guidance
Let’s Encrypt allows 50 certificates per registered domain per week. On a busy repository with many short-lived previews, the shared my.preview.run domain can hit this limit. To avoid it:
- Use one of the
rev*.clickbuilt-in domains (each is a separate registered domain with its own 50/week quota). - Distribute previews across multiple
rev*.clickdomains using thednsinput. - Use your own domain if you need full control over the quota.
A scheduled cleanup run (the examples use every 4 hours) recycles closed and expired environments so they don’t hold certificates unnecessarily.