Appearance
Production Deployment — Synkrato Console
Environment-specific deployment guide for Synkrato production. Follow the phases in azure-deployment.md — this document provides production-specific values, naming, and commands.
Environment Summary
| Property | Value |
|---|---|
| Environment | synkrato-prod |
| Resource Group | gg-sy-prod-console |
| Location | centralus |
| Console URL | https://console.synkrato.com |
| Zitadel Issuer | https://id.synkrato.com |
| PostgreSQL Server | console-synkrato-prod-pg |
| Container Apps Env | console-synkrato-prod-env |
| Managed Identity | console-synkrato-prod-id |
| Front Door Profile | console-synkrato-prod-fd |
| Storage Account (SPA) | synkratoconsoleweb |
| ACR | gaeadev.azurecr.io |
| Log Level | warn |
| Development Mode | false |
| Brand | Synkrato Console |
Scaling (Mission-Critical)
| Service | Min | Max | Notes |
|---|---|---|---|
| Console API | 2 | 5 | Zero-downtime deploys, AZ resilience |
| Console Worker | 1 | 3 | Background jobs |
| Zitadel | 2 | 5 | Auth must stay up during AZ outage |
Resilience Features
| Feature | Setting | Effect |
|---|---|---|
| Zone redundancy | zoneRedundant = true | Replicas spread across availability zones |
| VNet integration | enableVnet = true | Required for zone redundancy |
| PostgreSQL HA | postgresHaEnabled = true | Standby in different AZ, ~30s failover |
| PostgreSQL SKU | Standard_D2ds_v4 | GeneralPurpose (required for HA) |
| Backup retention | 35 days | Point-in-time restore |
| Geo-redundant backup | true | Backup replicated to paired Azure region |
| Front Door | Always on | Global edge, health probes, auto failover |
Step-by-Step
1. Prepare secrets
bash
cp infra/.env.synkrato-prod.example infra/.env.synkrato-prod
# Fill in all values — see infra/.env.synkrato-prod.example for the template2. 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-latestProduction tip: Consider using versioned tags (e.g.
v1.0.0orgit-<sha>) instead ofdev-latestfor traceability.
3. Deploy infrastructure
bash
./infra/deploy.sh synkrato-prod --phase infraThis 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-sy-prod-console \
--name console-synkrato-prod-pg \
--rule-name AllowMyIP \
--start-ip-address <YOUR_IP> \
--end-ip-address <YOUR_IP>
# Connect
psql "postgresql://consoleadmin:<POSTGRES_ADMIN_PASSWORD>@console-synkrato-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;
\qbash
# Remove firewall rule immediately
az postgres flexible-server firewall-rule delete \
--resource-group gg-sy-prod-console \
--name console-synkrato-prod-pg \
--rule-name AllowMyIP --yes5. Zitadel init (local Docker)
See Phase 4 in azure-deployment.md for the full Docker command. Use these production-specific values:
| Variable | Production Value |
|---|---|
ZITADEL_EXTERNALDOMAIN | id.synkrato.com |
ZITADEL_DATABASE_POSTGRES_HOST | console-synkrato-prod-pg.postgres.database.azure.com |
ZITADEL_FIRSTINSTANCE_ORG_HUMAN_EMAIL_ADDRESS | admin@synkrato.com |
ZITADEL_DEFAULTINSTANCE_SMTPCONFIGURATION_FROM | console@synkrato.com |
Critical: Save the
ZITADEL_MASTERKEYsecurely. 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.synkrato-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 synkrato-prod --phase zitadel_frontdoor7. Configure DNS
Retrieve values:
bash
ASUID=$(az containerapp env show -n console-synkrato-prod-env -g gg-sy-prod-console \
--query "properties.customDomainConfiguration.customDomainVerificationId" -o tsv)
FD_ID_HOST=$(az afd endpoint list --profile-name console-synkrato-prod-fd -g gg-sy-prod-console \
--query "[?starts_with(name,'id-')].hostName" -o tsv)
DNSAUTH_ID=$(az afd custom-domain show --profile-name console-synkrato-prod-fd -g gg-sy-prod-console \
--custom-domain-name id-synkrato-com \
--query "validationProperties.validationToken" -o tsv)Create DNS records:
| Type | Name | Value |
|---|---|---|
| TXT | asuid.id.synkrato.com | <customDomainVerificationId> |
| TXT | _dnsauth.id.synkrato.com | <validationToken> |
| CNAME | id.synkrato.com | <id-endpoint>.azurefd.net |
| TXT | asuid.console.synkrato.com | <customDomainVerificationId> |
Verify:
bash
curl -s https://id.synkrato.com/debug/ready
# Expected: ok
curl -s https://id.synkrato.com/.well-known/openid-configuration | jq .issuer
# Expected: "https://id.synkrato.com"8. Verify PAT tokens
bash
curl -s -H "Authorization: Bearer <admin-PAT>" \
https://id.synkrato.com/auth/v1/users/me | jq .user.userNameIf 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.synkrato.com
ZITADEL_PAT=<admin-human-user-pat>
CONSOLE_URL=https://console.synkrato.com
DEVELOPMENT_MODE=false
EOF
go run ./scripts/bootstrap/Copy output values from .bootstrap.env to infra/.env.synkrato-prod. Generate webhook secret:
bash
ZITADEL_WEBHOOK_SECRET=$(openssl rand -hex 32)
# Add to infra/.env.synkrato-prod10. Full stack deploy
Verify infra/.env.synkrato-prod has all required variables, then:
bash
./infra/deploy.sh synkrato-prod11. Console DNS + verify
bash
FD_CONSOLE_HOST=$(az afd endpoint list --profile-name console-synkrato-prod-fd -g gg-sy-prod-console \
--query "[?starts_with(name,'console-')].hostName" -o tsv)
DNSAUTH_CONSOLE=$(az afd custom-domain show --profile-name console-synkrato-prod-fd -g gg-sy-prod-console \
--custom-domain-name console-synkrato-com \
--query "validationProperties.validationToken" -o tsv)| Type | Name | Value |
|---|---|---|
| CNAME | console.synkrato.com | <console-endpoint>.azurefd.net |
| TXT | _dnsauth.console.synkrato.com | <validationToken> |
Important:
console.synkrato.comandid.synkrato.compoint to different Front Door endpoints.
bash
curl -s https://console.synkrato.com/api/healthz12. Deploy Web SPA
bash
cd web && npm run build && cd ..
./infra/deploy.sh synkrato-prod --deploy-web-only13. Re-run bootstrap (webhook)
The webhook target likely failed in step 9 because console.synkrato.com wasn't reachable. Now re-run:
bash
go run ./scripts/bootstrap/
./infra/deploy.sh synkrato-prod # pick up webhook secret if changedDay-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 synkrato-prodSPA-only update
bash
cd web && npm run build && cd ..
./infra/deploy.sh synkrato-prod --deploy-web-onlyDeploy specific image tag
bash
./infra/deploy.sh synkrato-prod --image-tag v1.0.0Preview changes (dry run)
bash
./infra/deploy.sh synkrato-prod --what-ifForce 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-sy-prod-console \
--set-env-vars "RESTART_TRIGGER=$(date +%s)"Production Checklist
Before deploying to production, verify:
- [ ] All secrets in
infra/.env.synkrato-prodare set (no empty values for required fields) - [ ] Docker images are pushed with the correct tag
- [ ] DNS records are created and propagated (
digto verify) - [ ] PostgreSQL firewall rule is removed after DB setup
- [ ] PAT tokens verified against the live Zitadel instance
- [ ] Bootstrap output values copied to
.env.synkrato-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.synkrato.com→ Zitadel → callback

