Installing USCP on Azure
Three paths. Read README.md first for DNS, OIDC, and licensing prerequisites.
- Path A — Terraform (recommended) — one
terraform apply
provisions the resource group, network, and VM and runs the installer via cloud-init.
- Path B — Azure VM + install.sh — a single VM driven by the
one-command installer (via cloud-init or by hand).
- Path C — AKS + Helm — HA, autoscaling, with Azure Database for
PostgreSQL.
Microsoft Entra ID is the natural OIDC provider here — its issuer is https://login.microsoftonline.com/<tenant-id>/v2.0 and the redirect URI must be https://<your-domain>/auth/callback. See post-install.md → Microsoft Entra ID.
Path A — Terraform (recommended)
The module in deploy/terraform/azure/ provisions a resource group, VNet/subnet, NSG (SSH locked to your CIDR; 80/443 public), public IP, NIC, and an Ubuntu 24.04 VM whose cloud-init runs deploy/install.sh unattended.
Prerequisites
- Terraform ≥ 1.5 and Azure CLI logged in (
az login). - An SSH key pair (
ssh-keygen -t ed25519).
Steps
git clone https://github.com/cfssay/doublelogic_apex uscp
cd uscp/codes/deploy/terraform/azure
terraform init
terraform apply \
-var="domain=uscp.example.com" \
-var="ssh_public_key=$(cat ~/.ssh/id_ed25519.pub)" \
-var="oidc_issuer=https://login.microsoftonline.com/<tenant-id>/v2.0" \
-var="oidc_client_id=<application-client-id>" \
-var="oidc_client_secret=<client-secret-value>" \
-var="acme_email=ops@example.com" \
-var="ssh_ingress_cidr=203.0.113.4/32"
Then:
terraform output public_ip→ create the DNS A record for your domain pointing at it.- Confirm the redirect URI
https://<domain>/auth/callbackis on your Entra ID app registration. - Watch bring-up:
ssh azureuser@<ip> sudo tail -f /var/log/uscp-bootstrap.log. - Verify
https://<domain>/readyz, then first login.
Optional vars: location, resource_group_name, vm_size, admin_username. For production Postgres, provision Azure Database for PostgreSQL and set USCP_DB_MODE=external + DATABASE_URL (see Path B). Full module notes: deploy/terraform/azure/README.md. terraform destroy tears it down (dump Postgres first).
Path B — Azure VM + install.sh
Prefer this for full control, or where you drive deploy/install.sh via cloud-init (deploy/cloud-init.yaml, supported natively as Azure custom data) or by hand.
Step 1 — Create a resource group and network rules
az group create --name uscp-rg --location eastus
# Network security group allowing SSH (your IP), HTTP, HTTPS
az network nsg create -g uscp-rg -n uscp-nsg
MYIP=$(curl -s https://ifconfig.me)
az network nsg rule create -g uscp-rg --nsg-name uscp-nsg -n ssh --priority 100 \
--destination-port-ranges 22 --source-address-prefixes "$MYIP/32" --access Allow --protocol Tcp
az network nsg rule create -g uscp-rg --nsg-name uscp-nsg -n http --priority 110 \
--destination-port-ranges 80 --source-address-prefixes '*' --access Allow --protocol Tcp
az network nsg rule create -g uscp-rg --nsg-name uscp-nsg -n https --priority 120 \
--destination-port-ranges 443 --source-address-prefixes '*' --access Allow --protocol Tcp
Port 80 must be open to the internet for the Let's Encrypt HTTP-01 challenge. Do not open 8080 or 5432.
Step 2 — Prepare cloud-init custom data
Copy deploy/cloud-init.yaml from the repo and edit the install.env block near the top — set your real values:
# inside deploy/cloud-init.yaml, the write_files block for /etc/uscp/install.env
USCP_DOMAIN=uscp.example.com
USCP_OIDC_ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0
USCP_OIDC_CLIENT_ID=<application-client-id>
USCP_OIDC_CLIENT_SECRET=<client-secret-value>
USCP_ACME_EMAIL=ops@example.com
USCP_REPO=https://github.com/cfssay/doublelogic_apex
The cloud-init script installs Go, clones USCP_REPO, and runs deploy/install.sh with those values on first boot. USCP_DATA_KEY is generated on the box by the installer and never leaves it.
Step 3 — Create the VM with that custom data
az vm create \
--resource-group uscp-rg \
--name uscp \
--image Ubuntu2204 \
--size Standard_B2s \ # 2 vCPU / 4 GB — the minimum
--admin-username azureuser \
--generate-ssh-keys \
--nsg uscp-nsg \
--public-ip-sku Standard \
--custom-data deploy/cloud-init.yaml
Note the publicIpAddress in the output.
Step 4 — Point DNS at the VM
Create A uscp.example.com → <publicIpAddress> and confirm:
dig +short uscp.example.com
Do this promptly — the cloud-init installer needs the name to resolve before it reaches the Caddy/ACME step.
Step 5 — Watch the install and verify
ssh azureuser@<publicIpAddress>
sudo tail -f /var/log/cloud-init-output.log # build → migrate → license → verify
curl -fsS https://uscp.example.com/healthz
curl -fsS https://uscp.example.com/readyz
If ACME failed because DNS wasn't ready yet, sudo systemctl restart caddy after it resolves.
Then continue with first login.
Manual variant (no cloud-init)
If you'd rather run the installer by hand:
az vm create -g uscp-rg -n uscp --image Ubuntu2204 --size Standard_B2s \
--admin-username azureuser --generate-ssh-keys --nsg uscp-nsg --public-ip-sku Standard
ssh azureuser@<ip>
sudo apt-get update && sudo apt-get install -y git docker.io && sudo systemctl enable --now docker
git clone https://github.com/cfssay/doublelogic_apex uscp && cd uscp/codes
sudo \
USCP_DOMAIN=uscp.example.com \
USCP_OIDC_ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0 \
USCP_OIDC_CLIENT_ID=<client-id> \
USCP_OIDC_CLIENT_SECRET=<client-secret> \
USCP_ACME_EMAIL=ops@example.com \
./deploy/install.sh
Using Azure Database for PostgreSQL (production)
Provision an Azure Database for PostgreSQL – Flexible Server (v16), create a database and a non-superuser role uscp_app, and pass it as an external database:
sudo \
USCP_DOMAIN=uscp.example.com \
USCP_OIDC_ISSUER=https://login.microsoftonline.com/<tenant-id>/v2.0 \
USCP_OIDC_CLIENT_ID=... USCP_OIDC_CLIENT_SECRET=... \
USCP_ACME_EMAIL=ops@example.com \
USCP_DB_MODE=external \
DATABASE_URL='postgres://uscp_app:PASSWORD@myserver.postgres.database.azure.com:5432/uscp?sslmode=require' \
./deploy/install.sh
- Never use the server admin account in
DATABASE_URL— it can bypass row-level security.
Create a dedicated non-superuser role. See post-install.md → Database hardening.
- Restrict the Flexible Server firewall / VNet rules to the VM's subnet only.
- Keep
sslmode=require.
Path C — AKS + Helm
For HA. The chart lives in deploy/helm/uscp/; see kubernetes.md for the full reference. Azure specifics:
Step 1 — Cluster, database, registry
# Container registry + AKS wired together
az acr create -g uscp-rg -n uscpacr --sku Standard
az aks create -g uscp-rg -n uscp-aks --node-count 3 --attach-acr uscpacr --generate-ssh-keys
az aks get-credentials -g uscp-rg -n uscp-aks
Provision Azure Database for PostgreSQL – Flexible Server (v16) in the same VNet, with a non-superuser uscp_app role.
Step 2 — Build and push the image
cd codes
az acr build --registry uscpacr --image uscp/controlplane:v1 . # builds ./Dockerfile in ACR
Step 3 — Secret + install
kubectl create namespace uscp
kubectl -n uscp create secret generic uscp-secrets \
--from-literal=DATABASE_URL='postgres://uscp_app:PASSWORD@myserver.postgres.database.azure.com:5432/uscp?sslmode=require' \
--from-literal=USCP_OIDC_ISSUER='https://login.microsoftonline.com/<tenant-id>/v2.0' \
--from-literal=USCP_OIDC_CLIENT_ID='...' \
--from-literal=USCP_OIDC_CLIENT_SECRET='...' \
--from-literal=USCP_AIRGAP_BUNDLE=/licenses/dev-entitlement.lic \
--from-literal=USCP_LICENSE_JWKS=/licenses/dev-jwks.json
helm install uscp deploy/helm/uscp -n uscp \
--set image.repository=uscpacr.azurecr.io/uscp/controlplane \
--set image.tag=v1 \
--set env.USCP_REGION=eastus \
--set env.USCP_PUBLIC_URL=https://uscp.example.com
Step 4 — Ingress, TLS, DNS
Expose the Service (port 8080) via an ingress controller with cert-manager (Let's Encrypt) or an Application Gateway with an uploaded certificate. Point uscp.example.com at the ingress IP, then:
curl -fsS https://uscp.example.com/readyz
Continue at kubernetes.md and post-install.md.