This guide shows you how to use your own domain (like api.mycompany.com) instead of the default Phala Cloud domain.
Prerequisites
- A domain with DNS hosted on Cloudflare (Linode and Namecheap also supported)
- Access to your DNS provider’s API token
- A deployed CVM application
Step 1: Create DNS API Token
Cloudflare
Linode
Namecheap
- Go to your Cloudflare Dashboard
- Select your domain
- Go to Get Your API Token
- Click Create Token
- Use the Edit zone DNS template
- Select your domain in Zone Resources
- Click Continue to summary and create the token
- Copy the token for later use
- Go to Linode Cloud Manager
- Click on your profile → API Tokens
- Create a Personal Access Token with Read/Write access to Domains
- Copy the token for later use
- Enable API access in your Namecheap account
- Go to Profile → Tools → API Access
- Note your username and API key
Step 2: Add dstack-ingress to Your Deployment
Update your docker-compose.yml to include the dstack-ingress container:
services:
dstack-ingress:
image: dstacktee/dstack-ingress:2.2@sha256:d05a7b343c37c1cca1bba8dbf7e8f3c6d2118158af2d41c455103796db4f67f0
ports:
- "443:443"
environment:
- DOMAIN=api.mycompany.com # Your custom domain
- TARGET_ENDPOINT=app:80 # Your app's internal endpoint
- CLOUDFLARE_API_TOKEN=${CLOUDFLARE_API_TOKEN}
- GATEWAY_DOMAIN=_.${DSTACK_GATEWAY_DOMAIN}
- CERTBOT_EMAIL=${CERTBOT_EMAIL}
- SET_CAA=true
volumes:
- /var/run/dstack.sock:/var/run/dstack.sock
- /var/run/tappd.sock:/var/run/tappd.sock
- cert-data:/etc/letsencrypt
- evidences:/evidences
restart: unless-stopped
app:
image: your-app:latest # Your application
volumes:
- evidences:/evidences:ro
# No ports exposed here - dstack-ingress handles external access
volumes:
cert-data: # Persistent storage for SSL certificates
evidences: # Generated attestation evidence files
Set DOMAIN to your custom domain and TARGET_ENDPOINT to your app’s internal endpoint. TARGET_ENDPOINT tells dstack-ingress where to forward incoming traffic:
app:80 - Forward to service named “app” on port 80
api:3000 - Forward to service named “api” on port 3000
http://app:80 and grpc://app:50051 are also accepted, but the protocol prefix is stripped before proxying
- Use the service name from docker-compose, not localhost
dstack-ingress is a HAProxy-based L4/TCP proxy. It terminates TLS and forwards the TCP stream to TARGET_ENDPOINT; it does not apply Nginx-style HTTP redirects, header rules, body limits, or buffering settings. Put HTTP-specific behavior in your application or in a separate HTTP proxy behind TARGET_ENDPOINT if needed.
In the Phala Cloud dashboard, add these encrypted secrets:
Cloudflare
Linode
Namecheap
CLOUDFLARE_API_TOKEN: Your API token from Step 1
CERTBOT_EMAIL: Your email for Let’s Encrypt notifications
LINODE_API_TOKEN: Your API token from Step 1
CERTBOT_EMAIL: Your email for Let’s Encrypt notifications
NAMECHEAP_USERNAME: Your Namecheap username
NAMECHEAP_API_KEY: Your API key from Step 1
NAMECHEAP_CLIENT_IP: Your whitelisted IP address
CERTBOT_EMAIL: Your email for Let’s Encrypt notifications
Step 4: Deploy
Deploy your updated docker-compose.yml through the dashboard. The first deployment typically takes 3–5 minutes as it:
- Configures DNS records
- Requests SSL certificate from Let’s Encrypt
- Starts the HAProxy TCP proxy
Step 5: Verify
After deployment, you can:
- Access your domain: Visit
https://api.mycompany.com
- Check certificate: Your browser should show a valid Let’s Encrypt certificate
- Verify evidence: Visit
https://api.mycompany.com/evidences/ to see attestation data
Multiple Custom Domains
You can route multiple custom domains from one ingress container by using DOMAINS and ROUTING_MAP:
services:
ingress:
image: dstacktee/dstack-ingress:2.2@sha256:d05a7b343c37c1cca1bba8dbf7e8f3c6d2118158af2d41c455103796db4f67f0
ports:
- "443:443"
environment:
DNS_PROVIDER: cloudflare
CLOUDFLARE_API_TOKEN: ${CLOUDFLARE_API_TOKEN}
CERTBOT_EMAIL: ${CERTBOT_EMAIL}
GATEWAY_DOMAIN: _.${DSTACK_GATEWAY_DOMAIN}
SET_CAA: true
DOMAINS: |
app.mycompany.com
api.mycompany.com
ROUTING_MAP: |
app.mycompany.com=frontend:3000
api.mycompany.com=api:8080
volumes:
- /var/run/dstack.sock:/var/run/dstack.sock
- /var/run/tappd.sock:/var/run/tappd.sock
- cert-data:/etc/letsencrypt
- evidences:/evidences
restart: unless-stopped
api:
image: my-api:latest
volumes:
- evidences:/evidences:ro
frontend:
image: my-frontend:latest
volumes:
- evidences:/evidences:ro
volumes:
cert-data:
evidences:
Why We Pin Infrastructure Images
The dstack-ingress image uses a SHA256 hash instead of a tag to ensure:
- Auditability: Anyone can verify the exact infrastructure code handling domains
- Immutability: The ingress behavior never changes unexpectedly
- Security: Prevents infrastructure image substitution
- Reproducibility: Domain handling is consistent across all deployments
Security Features
When SET_CAA=true is enabled:
- CAA records restrict certificate issuance to the TEE managed Let’s Encrypt account
- Only certificates requested from within your TEE can be issued
- Certificate transparency logs can be monitored for unauthorized certificates
Cloudflare Users: Cloudflare automatically overrides CAA records at the apex domain (e.g., example.com). This prevents domain attestation from working correctly.Solution options:
- Use a subdomain (recommended): Deploy to
app.example.com instead of example.com
- Disable Cloudflare’s CAA management: See Cloudflare CAA documentation
To verify your CAA record is working:dig @1.1.1.1 +short CAA your-domain.com
Should return the Let’s Encrypt account URI with validationmethods=dns-01;accounturi=...
Troubleshooting
Check Logs
Look for dstack-ingress container logs in the dashboard:
Successfully received certificate.
Generated evidences successfully
Next renewal check in 12 hours
Certbot Error: “too many certificates (5) already issued for this exact set of identifiers”
You may have forgotten to attach the data volume.
Other Common Issues
DNS not propagating: Wait 2-5 minutes for DNS changes to propagate globally.
Certificate request failed: Verify your API token has permission to edit DNS records.
Connection refused: Ensure TARGET_ENDPOINT points to the correct service:port.
CAA record conflicts: Some providers (like Linode) don’t allow CAA and CNAME on the same subdomain. The system will automatically use A records instead.
Next Steps