Skip to main content
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

  1. Go to your Cloudflare Dashboard
  2. Select your domain
  3. Go to Get Your API Token
  4. Click Create Token
  5. Use the Edit zone DNS template
  6. Select your domain in Zone Resources
  7. Click Continue to summary and create the token
  8. Copy the token for later use

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.

Step 3: Configure Environment Variables

In the Phala Cloud dashboard, add these encrypted secrets:
  • CLOUDFLARE_API_TOKEN: Your API token from Step 1
  • 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:
  1. Configures DNS records
  2. Requests SSL certificate from Let’s Encrypt
  3. Starts the HAProxy TCP proxy

Step 5: Verify

After deployment, you can:
  1. Access your domain: Visit https://api.mycompany.com
  2. Check certificate: Your browser should show a valid Let’s Encrypt certificate
  3. 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:
  1. Use a subdomain (recommended): Deploy to app.example.com instead of example.com
  2. 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