Skip to content

Custom Domains

Custom domain setup is a three-stage process split across two Bicep templates (shared.bicep and main.bicep) and two workflows.

Architecture

shared.bicep  →  dna-shared-portfolio-westeurope-rg
  └── Azure DNS Zone (sven-relijveld.com)
       ├── TXT  asuid       → prod CA env verification ID
       ├── TXT  asuid.www   → prod CA env verification ID
       ├── TXT  asuid.dev   → dev CA env verification ID
       ├── CNAME www        → prod frontend Container App FQDN
       ├── CNAME dev        → dev frontend Container App FQDN
       ├── CNAME docs       → prod SWA default hostname
       └── CNAME dev-docs   → dev SWA default hostname

main.bicep  →  dna-{env}-portfolio-{region}-rg
  └── Container Apps Environment
       ├── Managed Certificate  www.sven-relijveld.com  [prod, SniEnabled]
       └── Managed Certificate  dev.sven-relijveld.com  [dev, SniEnabled]

The Three-Stage Process

Azure Container Apps requires a hostname to be registered in the environment before a managed TLS certificate can be issued.

Stage 1 — No binding

custom_domain is empty (or deploy_custom_domain=false, customDomains: []).

This is the initial state. All infra is deployed without any custom domain configuration.

Stage 2 — Disabled binding (hostname registration)

custom_domain is set, deploy_custom_domain=falsebindingType: 'Disabled'.

The hostname is registered in the Container Apps Environment so Azure can validate DNS, but no certificate is issued yet. DNS records must exist at this point.

Stage 3 — SniEnabled (managed TLS cert active)

deploy_custom_domain=truebindingType: 'SniEnabled'.

Azure issues a managed certificate and attaches it. Both parameters.prod.bicepparam and parameters.dev.bicepparam are at Stage 3.

Deploy order matters

deploy-infrastructure.yml always overrides deploy_custom_domain=false for the base infra step — cert issuance only activates via deploy-application.yml or a manual dispatch with deploy_containers=true.

Workflow: deploy-infrastructure-shared.yml

Triggers: push to main touching infra/shared.bicep, infra/modules/dns_zone.bicep, infra/modules/app_service_domain.bicep, infra/parameters.shared.bicepparam, or the workflow file; workflow_dispatch

What it does:

  1. Discover prod values — queries prod CAE customDomainVerificationId and frontend FQDN via az rest (ARM API)
  2. Discover dev + SWA values — queries dev CAE verification ID, dev frontend FQDN, prod SWA default hostname, dev SWA default hostname
  3. Deploys shared.bicep with all discovered values as parameters
  4. Creates DNS records conditionally — each record is only created if the discovered value is non-empty
  5. If purchase_domain: true: fetches TLD agreement keys, determines runner public IP, validates domain availability, purchases domain

Discovery steps emit empty strings gracefully when target resources don't exist yet — no hard failures.

Duration: ~3–5 minutes

Step-by-Step: Setting Up Custom Domains from Scratch

Step 1: Deploy base infrastructure (prod)

Push to main or run:

bash
gh workflow run deploy-infrastructure.yml --ref main \
  --field environment=prod \
  --field deploy_containers=false

This creates the Container Apps Environment. The verification ID and frontend FQDN are now discoverable.

Step 2: Run shared infrastructure workflow

bash
gh workflow run deploy-infrastructure-shared.yml --ref main

This creates the DNS zone and all DNS records (TXT + CNAME) for prod. For dev, records are only created after dev infra exists.

Step 3: Purchase the domain (first time only)

In infra/parameters.shared.bicepparam:

bicep
purchase_domain: true

Commit and push to main (or workflow_dispatch). NS delegation is automatic — no registrar changes needed.

Step 4: Enable managed cert (prod)

In infra/parameters.prod.bicepparam:

bicep
deploy_custom_domain: true

Commit and push. The next deploy-application.yml run will issue the managed cert.

Step 5: Enable SWA custom domain (docs)

In infra/parameters.prod.bicepparam:

bicep
deploy_swa_custom_domain: true

Commit and push. The SWA custom domain uses CNAME delegation — no cert to issue separately.

Static Web App Custom Domains

SWA custom domains (docs.sven-relijveld.com, dev-docs.sven-relijveld.com) use CNAME delegation rather than managed certs. The DNS CNAME records point to the SWA default hostname, which is discovered at runtime by the shared workflow.

Domain Parameter Status
docs.sven-relijveld.com deploy_swa_custom_domain: true in prod params Active
dev-docs.sven-relijveld.com deploy_swa_custom_domain: true in dev params Active

Sensitive Registrant Fields

parameters.shared.bicepparam stores only non-sensitive registrant fields (name, email, country). The following are injected at workflow runtime from GitHub Secrets:

GitHub Secret Field
CONTACT_PHONE Phone number
CONTACT_ADDRESS1 Street address
CONTACT_CITY City
CONTACT_STATE State/province
CONTACT_POSTAL_CODE Postal code

This keeps PII out of git history.

Current Domain Status

Domain Type Status
www.sven-relijveld.com Container App (prod) Active — SniEnabled managed cert
dev.sven-relijveld.com Container App (dev) Active — SniEnabled managed cert
docs.sven-relijveld.com Static Web App (prod) Active — CNAME delegation
dev-docs.sven-relijveld.com Static Web App (dev) Active — CNAME delegation

Troubleshooting

DNS records not resolving

Run the shared infrastructure workflow to ensure all DNS records exist:

bash
gh workflow run deploy-infrastructure-shared.yml --ref main

Then verify:

bash
nslookup www.sven-relijveld.com
dig TXT asuid.sven-relijveld.com

Managed cert not issued

Ensure Stage 2 has completed (hostname registered with Disabled binding) before attempting Stage 3. The DNS TXT record (asuid.www) must resolve to the Container Apps Environment verification ID.

bindingType: 'Disabled' still triggers validation

This is expected — Disabled still registers the hostname in the environment and triggers Azure's TXT record validation. It does not issue a cert. Only SniEnabled issues a cert.

Next Steps

Page history

Field Value
Last updated 2026-03-10

Changelog

Date PRs Summary
2026-03-10 #66 Extracted custom domains page from workflow-execution-order; 3-stage cert process, shared.bicep, DNS records