This guide explains how to configure dstack-ingress to work with different DNS providers for managing custom domains and SSL certificates.
- Cloudflare - The original and default provider
- Linode DNS - For Linode-hosted domains
- Namecheap - For Namecheap-hosted domains
- Route53 - For AWS hosted domains
DOMAIN- Your custom domain (e.g.,app.example.com)GATEWAY_DOMAIN- dstack gateway domain (e.g.,_.dstack-prod5.phala.network)CERTBOT_EMAIL- Email for Let's Encrypt registrationTARGET_ENDPOINT- Backend application endpoint to proxy toDNS_PROVIDER- DNS provider to use (cloudflare,linode,namecheap,route53)
SET_CAA- Enable CAA record setup (default: false)PORT- HTTPS port (default: 443)TXT_PREFIX- Prefix for TXT records (default: "_tapp-address")ALIAS_DOMAIN- A shared domain that acts as a load-balanced entry point across multiple Phala nodes (e.g.,app.example.com). Each node automatically joins the upstream pool on boot — users hit one address while traffic is distributed across however many nodes are running. See Weighted Routing with ALIAS_DOMAIN.
DNS_PROVIDER=cloudflare
CLOUDFLARE_API_TOKEN=your-api-tokenRequired Permissions:
- Zone:Read
- DNS:Edit
DNS_PROVIDER=linode
LINODE_API_TOKEN=your-api-tokenRequired Permissions:
- Domains: Read/Write access
Important Note for Linode:
- Linode has a limitation where CAA and CNAME records cannot coexist on the same subdomain
- To work around this, the system will attempt to use A records instead of CNAME records
- If the gateway domain can be resolved to an IP, an A record will be created
- If resolution fails, it falls back to CNAME (but CAA records won't work on that subdomain)
- This is a Linode-specific limitation not present in other providers
DNS_PROVIDER=namecheap
NAMECHEAP_USERNAME=your-username
NAMECHEAP_API_KEY=your-api-key
NAMECHEAP_CLIENT_IP=your-client-ipRequired Credentials:
NAMECHEAP_USERNAME- Your Namecheap account usernameNAMECHEAP_API_KEY- Your Namecheap API key (from https://ap.www.namecheap.com/settings/tools/apiaccess/)NAMECHEAP_CLIENT_IP- The IP address of the node (required for Namecheap API authentication)
Important Notes for Namecheap:
- Namecheap API requires node IP address for authentication, and you need add it to whitelist IP first.
- Namecheap doesn't support CAA records through their API currently
- The certbot plugin uses the format
certbot-dns-namecheappackage
DNS_PROVIDER=route53
AWS_ACCESS_KEY_ID=service-account-key-that-can-assume-role
AWS_SECRET_ACCESS_KEY=service-account-secret-that-can-assume-role
AWS_ROLE_ARN=role-that-can-mod-route53
AWS_REGION=your-closest-regionRequired Permissions:
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowDnsChallengeChanges
Effect: Allow
Action:
- route53:ChangeResourceRecordSets
Resource: !Sub arn:aws:route53:::hostedzone/${HostedZoneId}
- Sid: AllowListingForDnsChallenge
Effect: Allow
Action:
- route53:ListHostedZonesByName
- route53:ListHostedZones
- route53:GetChange
- route53:ListResourceRecordSetsOptional Variables for Route53:
ROUTE53_INITIAL_WEIGHT- Enables Weighted Routing on the CNAME record created forDOMAIN(e.g.node1.app.example.com → phala gateway). Set to an integer (e.g.,100) to assign that weight to the record. A uniqueSetIdentifieris generated automatically. TXT records are never weighted. Omit to create a standard non-weighted CNAME. WhenALIAS_DOMAINis also set, this variable additionally triggers creation of a weight-0 weighted CNAMEALIAS_DOMAIN → DOMAIN— see below.ALIAS_DOMAIN- See Weighted Routing with ALIAS_DOMAIN.
Important Notes for Route53:
- The certbot plugin uses the
certbot-dns-route53package - CAA will merge AWS & Let's Encrypt CA domains into existing records if they exist
- It is essential that the AWS service account used can only assume the limited role. See cloudformation example.
version: '3.8'
services:
ingress:
image: dstack-ingress:latest
ports:
- "443:443"
environment:
# Common configuration
- DNS_PROVIDER=linode
- DOMAIN=app.example.com
- GATEWAY_DOMAIN=_.dstack-prod5.phala.network
- CERTBOT_EMAIL=admin@example.com
- TARGET_ENDPOINT=http://backend:8080
# Linode specific
- LINODE_API_TOKEN=your-api-token
volumes:
- ./letsencrypt:/etc/letsencrypt
- ./evidences:/evidencesversion: '3.8'
services:
ingress:
image: dstack-ingress:latest
ports:
- "443:443"
environment:
# Common configuration
- DNS_PROVIDER=namecheap
- DOMAIN=app.example.com
- GATEWAY_DOMAIN=_.dstack-prod5.phala.network
- CERTBOT_EMAIL=admin@example.com
- TARGET_ENDPOINT=http://backend:8080
# Namecheap specific
- NAMECHEAP_USERNAME=your-username
- NAMECHEAP_API_KEY=your-api-key
- NAMECHEAP_CLIENT_IP=your-public-ip
volumes:
- ./letsencrypt:/etc/letsencrypt
- ./evidences:/evidencesservices:
dstack-ingress:
image: dstack-ingress:latest
restart: unless-stopped
volumes:
- /var/run/dstack.sock:/var/run/dstack.sock
- cert-data:/etc/letsencrypt
ports:
- 443:443
environment:
DNS_PROVIDER: route53
DOMAIN: app.example.com
GATEWAY_DOMAIN: _.${DSTACK_GATEWAY_DOMAIN}
AWS_REGION: ${AWS_REGION}
AWS_ROLE_ARN: ${AWS_ROLE_ARN}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
CERTBOT_EMAIL: ${CERTBOT_EMAIL}
TARGET_ENDPOINT: http://backend:8080
SET_CAA: 'true'
volumes:
cert-data:For multi-node weighted routing, see Weighted Routing with ALIAS_DOMAIN.
This pattern lets you run multiple independent TEE nodes behind a single public domain using Route53 weighted routing. Each node manages its own Phala identity (TXT record, CNAME to gateway) while also being registered as a weighted target for the shared public domain. Per-node domains also prevent hitting Let's Encrypt's duplicate-certificate rate limits that would occur if every node requested a cert for the same shared domain.
Public domain (shared across nodes)
────────────────────────────────────────────────────────────────
app.example.com CNAME node1.app.example.com weight=100 id=node1.app.example.com
app.example.com CNAME node2.app.example.com weight=0 id=node2.app.example.com ← new, dark
_dstack-app-address.app.example.com TXT <appid1>:443 ← one entry per node in pool,
<appid2>:443 appended on each node boot
Per-node records (managed by each node's dstack-ingress)
────────────────────────────────────────────────────────────────
node1.app.example.com CNAME <appid1>.dstack-prod5.phala.network weight=100
node2.app.example.com CNAME <appid2>.dstack-prod5.phala.network weight=100
_dstack-app-address.node1.app.example.com TXT <appid1>:443
_dstack-app-address.node2.app.example.com TXT <appid2>:443
TLS certificate (on each node)
────────────────────────────────────────────────────────────────
Subject: node1.app.example.com
SAN: app.example.com
ALIAS_DOMAIN |
ROUTE53_INITIAL_WEIGHT |
Cert SAN | Nginx server_name | Weight-0 CNAME for ALIAS_DOMAIN |
|---|---|---|---|---|
| not set | any | no | no | no |
| set | not set | yes | yes | no |
| set | set | yes | yes | yes |
When a new node starts with both variables set, dstack-ingress performs these steps automatically:
1. CNAME node2.app.example.com → <appid2>.dstack.phala.network (weighted, ROUTE53_INITIAL_WEIGHT)
2. TXT _dstack-app-address.node2.app.example.com → <appid2>:443
3. CNAME app.example.com → node2.app.example.com (weight=0, SetIdentifier=node2.app.example.com)
4. Obtain TLS cert for node2.app.example.com + app.example.com (SAN)
The node is fully verified and ready before step 3 introduces it to the pool. Traffic only starts flowing after an operator explicitly sets the weight above 0 in Route53.
Node 1 (node1.app.example.com):
services:
dstack-ingress:
image: dstack-ingress:latest
restart: unless-stopped
ports:
- "443:443"
environment:
DNS_PROVIDER: route53
DOMAIN: node1.app.example.com
ALIAS_DOMAIN: app.example.com
GATEWAY_DOMAIN: _.${DSTACK_GATEWAY_DOMAIN}
CERTBOT_EMAIL: ${CERTBOT_EMAIL}
TARGET_ENDPOINT: http://app:80
SET_CAA: 'true'
ROUTE53_INITIAL_WEIGHT: '100'
AWS_REGION: ${AWS_REGION}
AWS_ROLE_ARN: ${AWS_ROLE_ARN}
AWS_ACCESS_KEY_ID: ${AWS_ACCESS_KEY_ID}
AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY}
volumes:
- /var/run/dstack.sock:/var/run/dstack.sock
- cert-data:/etc/letsencrypt
volumes:
cert-data:Node 2 (node2.app.example.com) uses identical config with DOMAIN: node2.app.example.com. On first boot it registers itself at weight 0. To promote it, update the record weight in Route53.
For a complete production-ready reference that includes a dynamic nginx upstream manager (automatically enrolling and unenrolling backend containers as they start and stop), see docker-compose.loadbalanced.yaml in this repository.
- The weight-0 CNAME for
ALIAS_DOMAINusesDOMAINas theSetIdentifier, so each node has a stable, unique slot in the weighted record set that survives restarts without creating duplicates. - For non-Route53 providers (Cloudflare, Linode, Namecheap),
ALIAS_DOMAINstill adds the SAN and updates nginx, but those providers do not support weighted CNAME records at the DNS level. You would need to manage traffic distribution through the provider's own load balancing features. ALIAS_DOMAINmust be in the same Route53 hosted zone asDOMAIN, or at least a zone your credentials can write to.
If you're currently using the Cloudflare-only version:
- No changes needed for Cloudflare users - The default behavior remains Cloudflare
- For other providers - Add the
DNS_PROVIDERenvironment variable and provider-specific credentials
If you see "Could not detect DNS provider type", ensure you have either:
- Set
DNS_PROVIDERenvironment variable explicitly, OR - Set provider-specific credential environment variables (e.g.,
CLOUDFLARE_API_TOKEN)
Different providers may have different propagation times. The default is 120 seconds, but you may need to adjust based on your provider's behavior.
Ensure your API tokens/credentials have the necessary permissions listed above for your provider.
- Go to https://dash.cloudflare.com/profile/api-tokens
- Create token with Zone:Read and DNS:Edit permissions
- Scope to specific zones if desired
- Go to https://cloud.linode.com/profile/tokens
- Create a Personal Access Token
- Grant "Domains" Read/Write access
- Go to https://ap.www.namecheap.com/settings/tools/api-access/
- Enable API access for your account
- Note down your API key and username
- Make sure your IP address is whitelisted in the API settings