Domains & TLS
Custom domains, automatic TLS via Let's Encrypt, DNS configuration, and domain monitoring.
Vardo uses Traefik as its reverse proxy. Traefik discovers containers via Docker labels and handles TLS certificate provisioning automatically.
How it works
When you add a domain to an app, Vardo injects Traefik labels into the compose file at deploy time. Traefik reads those labels from running containers on the vardo-network Docker network and starts routing traffic — no config reload needed.
Adding a custom domain
Go to your app -> Domains -> Add Domain.
Enter the domain (e.g. app.example.com).
Configure DNS (see below).
Deploy the app. Traefik requests a TLS certificate automatically.
Each domain is associated with one app and one container port. Multiple domains can point to the same app.
Domain fields
| Field | Description |
|---|---|
domain | Fully qualified domain name |
port | Container port for this domain (overrides app default) |
certResolver | Certificate resolver to use (default: le) |
sslEnabled | Enable HTTPS (default: true) |
isPrimary | Mark as the primary domain |
serviceName | Target a specific service in a multi-service compose file |
DNS configuration
Point your domain at the server running Vardo.
A record (recommended for root domains):
app.example.com. A <your-server-ip>CNAME record (for subdomains):
app.example.com. CNAME your-vardo-host.example.com.DNS changes typically propagate in minutes but can take up to 48 hours. Traefik won't issue a certificate until DNS resolves to the server.
Wildcard domains
Wildcard domains (e.g. *.example.com) require a DNS challenge for certificate issuance. Configure the DNS provider credentials in your Traefik configuration.
Wildcard certificate support via DNS challenge requires additional Traefik configuration not managed by Vardo directly.
Automatic TLS
When sslEnabled: true (the default), Vardo injects these Traefik labels:
traefik.http.routers.{routerName}.entrypoints: websecure
traefik.http.routers.{routerName}.tls: "true"
traefik.http.routers.{routerName}.tls.certresolver: le
# HTTP -> HTTPS redirect
traefik.http.routers.{routerName}-http.entrypoints: web
traefik.http.routers.{routerName}-http.middlewares: {routerName}-https-redirect
traefik.http.middlewares.{routerName}-https-redirect.redirectscheme.scheme: https
traefik.http.middlewares.{routerName}-https-redirect.redirectscheme.permanent: "true"Traefik requests a certificate using the HTTP-01 challenge on port 80. Certificates are stored in Traefik's ACME storage and renewed automatically before expiry.
Multiple ACME issuers
Vardo supports three certificate issuers out of the box:
| Issuer | Resolver key | Notes |
|---|---|---|
| Let's Encrypt | le | Default. Free, widely trusted. |
| Google Trust Services | google | Google's public ACME CA. |
| ZeroSSL | zerossl | Requires External Account Binding (EAB) credentials. |
Configuring the default issuer
Set the default in vardo.yml:
ssl:
defaultIssuer: le # le | google | zerosslFor ZeroSSL, add EAB credentials in vardo.secrets.yml:
zerossl:
eabKid: "<your EAB KID>"
eabHmac: "<your EAB HMAC key>"You can also configure these via the admin UI under Settings -> SSL.
Per-domain cert resolver
Override the default issuer on any individual domain by setting the certResolver field in domain settings. This lets you use Let's Encrypt for most domains but ZeroSSL or Google for specific ones.
HTTP-only mode
To disable TLS for a domain, set sslEnabled: false. Vardo routes the domain on the web entrypoint without TLS or redirect.
.localhost domains for development
Domains ending in .localhost get special treatment:
- TLS is configured but no cert resolver is set — Traefik generates a self-signed certificate
- No DNS needed —
*.localhostresolves to127.0.0.1on most systems
Example: myapp.localhost works out of the box for local development.
Auto-generated subdomains
When you create an app, Vardo can generate a subdomain automatically:
| Type | Pattern | Example |
|---|---|---|
| App | {appName}-{adjective}-{noun}.{baseDomain} | myapp-spicy-mango.example.com |
| Environment | {appName}-{envName}.{baseDomain} | myapp-staging.example.com |
| Preview | {appName}-pr-{prNumber}.{baseDomain} | myapp-pr-42.example.com |
The base domain is configured per-organization (org.baseDomain), with a fallback to VARDO_BASE_DOMAIN from the environment.
Domain health checks
Vardo monitors all domains attached to active apps every 5 minutes:
- Probes each domain via HTTPS with a 10-second timeout
- Falls back to HTTP if HTTPS fails
- Records a
domain_checkentry: reachable status, HTTP status code, response time - Keeps the last 100 checks per domain (older entries are pruned)
A domain is considered reachable if the HTTP status code is < 500.
State transition alerts
When a domain transitions from reachable to unreachable, Vardo logs a warning:
[domain-monitor] WARNING: app.example.com (app: myapp) is unreachable — connect ECONNREFUSEDIn the UI, each domain shows its last check status and response time. The API exposes check history per domain.
Traefik network
All Vardo-managed containers attach to the vardo-network Docker bridge network. Traefik must be on this network to discover containers.
networks:
vardo-network:
external: trueVardo creates the network automatically if it doesn't exist during deploy.
Cloudflare configuration
If you're using Cloudflare as your DNS provider:
DNS settings:
- Set records to DNS only (grey cloud) while setting up TLS. Switch to proxied once Let's Encrypt certificates are working.
- For Full (Strict) SSL mode, Cloudflare requires a valid origin certificate. Let's Encrypt satisfies this.
SSL/TLS settings:
- Mode: Full (Strict)
- Always use HTTPS: enabled
- Automatic HTTPS Rewrites: enabled
With Cloudflare's proxy (orange cloud), HTTP-01 ACME challenges still work — but ensure port 80 is accessible from Cloudflare's servers.
Troubleshooting
API
GET /api/v1/organizations/{orgId}/domains
POST /api/v1/organizations/{orgId}/apps/{appId}/domains
GET /api/v1/organizations/{orgId}/apps/{appId}/domains
PUT /api/v1/organizations/{orgId}/apps/{appId}/domains/{domainId}
DELETE /api/v1/organizations/{orgId}/apps/{appId}/domains/{domainId}