Skip to content

Production Deployment — Exto Console

Environment-specific deployment guide for Exto production. Follow the phases in azure-deployment.md — this document provides production-specific values, naming, and commands.

Environment Summary

PropertyValue
Environmentexto-prod
Resource Groupgg-ex-prod-console
Locationcentralus
Console URLhttps://console.exto360.com
Zitadel Issuerhttps://id.exto360.com
PostgreSQL Serverconsole-prod-pg
Container Apps Envconsole-prod-env
Managed Identityconsole-prod-id
Front Door Profileconsole-prod-fd
Storage Account (SPA)extoconsoleweb
ACRgaeadev.azurecr.io
Log Levelwarn
Development Modefalse
BrandExto Console

Scaling (Mission-Critical)

ServiceMinMaxNotes
Console API25Zero-downtime deploys, AZ resilience
Console Worker13Background jobs
Zitadel25Auth must stay up during AZ outage

Resilience Features

FeatureSettingEffect
Zone redundancyzoneRedundant = trueReplicas spread across availability zones
VNet integrationenableVnet = trueRequired for zone redundancy
PostgreSQL HApostgresHaEnabled = trueStandby in different AZ, ~30s failover
PostgreSQL SKUStandard_D2ds_v4GeneralPurpose (required for HA)
Backup retention35 daysPoint-in-time restore
Geo-redundant backuptrueBackup replicated to paired Azure region
Front DoorAlways onGlobal edge, health probes, auto failover

Step-by-Step

1. Prepare secrets

bash
cp infra/.env.exto-prod.example infra/.env.exto-prod
# Fill in all values — see infra/.env.exto-prod.example for the template

2. Build and push images

bash
make docker-all REGISTRY=gaeadev.azurecr.io TAG=dev-latest
az acr login -n gaeadev --subscription <GaeaGlobal-subscription-id>
docker push gaeadev.azurecr.io/console-api:dev-latest
docker push gaeadev.azurecr.io/console-worker:dev-latest
docker push gaeadev.azurecr.io/console-web:dev-latest

Production tip: Consider using versioned tags (e.g. v1.0.0 or git-<sha>) instead of dev-latest for traceability.

3. Deploy infrastructure

bash
./infra/deploy.sh exto-prod --phase infra

This creates: Resource Group, PostgreSQL Flexible Server (with HA), VNet, Key Vault, Storage Account, Log Analytics, Managed Identity, Container Apps Environment (zone-redundant), and ACR Pull role.

4. Database setup

bash
# Allow your IP
az postgres flexible-server firewall-rule create \
  --resource-group gg-ex-prod-console \
  --name console-prod-pg \
  --rule-name AllowMyIP \
  --start-ip-address <YOUR_IP> \
  --end-ip-address <YOUR_IP>

# Connect
psql "postgresql://consoleadmin:<POSTGRES_ADMIN_PASSWORD>@console-prod-pg.postgres.database.azure.com:5432/postgres?sslmode=require"
sql
CREATE ROLE zitadel LOGIN PASSWORD '<ZITADEL_DB_PASSWORD>';
ALTER DATABASE zitadel_auth OWNER TO zitadel;

CREATE ROLE console_app LOGIN PASSWORD '<CONSOLE_DB_PASSWORD>';
GRANT azure_pg_admin TO console_app;
CREATE DATABASE console OWNER console_app;
\c console
GRANT ALL PRIVILEGES ON SCHEMA public TO console_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO console_app;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON SEQUENCES TO console_app;
\q
bash
# Remove firewall rule immediately
az postgres flexible-server firewall-rule delete \
  --resource-group gg-ex-prod-console \
  --name console-prod-pg \
  --rule-name AllowMyIP --yes

5. Zitadel init (local Docker)

See Phase 4 in azure-deployment.md for the full Docker command. Use these production-specific values:

VariableProduction Value
ZITADEL_EXTERNALDOMAINid.exto360.com
ZITADEL_DATABASE_POSTGRES_HOSTconsole-prod-pg.postgres.database.azure.com
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL_ADDRESSadmin@exto360.com
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_FROMconsole@exto360.com

Critical: Save the ZITADEL_MASTERKEY securely. The deployed Zitadel container must use the exact same key. If they diverge, all PAT tokens become invalid (Errors.Token.Invalid).

Extract PAT tokens from the init log and add ZITADEL_LOGIN_SERVICE_TOKEN to infra/.env.exto-prod.

6. Deploy Zitadel + Front Door

WARNING: Do NOT use --phase zitadel (without _frontdoor). See azure-deployment.md Phase 5 for why.

bash
./infra/deploy.sh exto-prod --phase zitadel_frontdoor

7. Configure DNS

Retrieve values:

bash
ASUID=$(az containerapp env show -n console-prod-env -g gg-ex-prod-console \
  --query "properties.customDomainConfiguration.customDomainVerificationId" -o tsv)

FD_ID_HOST=$(az afd endpoint list --profile-name console-prod-fd -g gg-ex-prod-console \
  --query "[?starts_with(name,'id-')].hostName" -o tsv)

DNSAUTH_ID=$(az afd custom-domain show --profile-name console-prod-fd -g gg-ex-prod-console \
  --custom-domain-name id-exto360-com \
  --query "validationProperties.validationToken" -o tsv)

Create DNS records:

TypeNameValue
TXTasuid.id.exto360.com<customDomainVerificationId>
TXT_dnsauth.id.exto360.com<validationToken>
CNAMEid.exto360.com<id-endpoint>.azurefd.net
TXTasuid.console.exto360.com<customDomainVerificationId>

Verify:

bash
curl -s https://id.exto360.com/debug/ready
# Expected: ok

curl -s https://id.exto360.com/.well-known/openid-configuration | jq .issuer
# Expected: "https://id.exto360.com"

8. Verify PAT tokens

bash
curl -s -H "Authorization: Bearer <admin-PAT>" \
  https://id.exto360.com/auth/v1/users/me | jq .user.userName

If you see Errors.Token.Invalid, the masterkey doesn't match. See Troubleshooting in azure-deployment.md.

9. Bootstrap

bash
cat > .bootstrap-input.env << 'EOF'
ZITADEL_URL=https://id.exto360.com
ZITADEL_PAT=<admin-human-user-pat>
CONSOLE_URL=https://console.exto360.com
DEVELOPMENT_MODE=false
EOF

go run ./scripts/bootstrap/

Copy output values from .bootstrap.env to infra/.env.exto-prod. Generate webhook secret:

bash
ZITADEL_WEBHOOK_SECRET=$(openssl rand -hex 32)
# Add to infra/.env.exto-prod

10. Full stack deploy

Verify infra/.env.exto-prod has all required variables (see Phase 7 checklist in azure-deployment.md), then:

bash
./infra/deploy.sh exto-prod

11. Console DNS + verify

bash
FD_CONSOLE_HOST=$(az afd endpoint list --profile-name console-prod-fd -g gg-ex-prod-console \
  --query "[?starts_with(name,'console-')].hostName" -o tsv)

DNSAUTH_CONSOLE=$(az afd custom-domain show --profile-name console-prod-fd -g gg-ex-prod-console \
  --custom-domain-name console-exto360-com \
  --query "validationProperties.validationToken" -o tsv)
TypeNameValue
CNAMEconsole.exto360.com<console-endpoint>.azurefd.net
TXT_dnsauth.console.exto360.com<validationToken>

Important: console.exto360.com and id.exto360.com point to different Front Door endpoints.

bash
curl -s https://console.exto360.com/api/healthz

12. Deploy Web SPA

bash
cd web && npm run build && cd ..
./infra/deploy.sh exto-prod --deploy-web-only

13. Re-run bootstrap (webhook)

The webhook target likely failed in step 9 because console.exto360.com wasn't reachable. Now re-run:

bash
go run ./scripts/bootstrap/
./infra/deploy.sh exto-prod   # pick up webhook secret if changed

Day-to-Day Operations

Redeploy after code changes

bash
make docker-all REGISTRY=gaeadev.azurecr.io TAG=dev-latest
docker push gaeadev.azurecr.io/console-api:dev-latest
docker push gaeadev.azurecr.io/console-worker:dev-latest
./infra/deploy.sh exto-prod

SPA-only update

bash
cd web && npm run build && cd ..
./infra/deploy.sh exto-prod --deploy-web-only

Deploy specific image tag

bash
./infra/deploy.sh exto-prod --image-tag v1.0.0

Preview changes (dry run)

bash
./infra/deploy.sh exto-prod --what-if

Force restart a container (e.g. after secret change)

Secret changes alone do NOT trigger a new revision. Force restart:

bash
az containerapp update -n console-api -g gg-ex-prod-console \
  --set-env-vars "RESTART_TRIGGER=$(date +%s)"

Production Checklist

Before deploying to production, verify:

  • [ ] All secrets in infra/.env.exto-prod are set (no empty values for required fields)
  • [ ] Docker images are pushed with the correct tag
  • [ ] DNS records are created and propagated (dig to verify)
  • [ ] PostgreSQL firewall rule is removed after DB setup
  • [ ] PAT tokens verified against the live Zitadel instance
  • [ ] Bootstrap output values copied to .env.exto-prod
  • [ ] Webhook secret generated and set
  • [ ] Full stack deployed and healthz endpoints return OK
  • [ ] SPA deployed and accessible
  • [ ] End-to-end login flow tested: console.exto360.com → Zitadel → callback