Status: 104 capabilities — 20 GA, 84 Beta. Enforcement is monitor-only by default. Honest status →

Installing USCP on Azure

Three paths. Read README.md first for DNS, OIDC, and licensing prerequisites.

provisions the resource group, network, and VM and runs the installer via cloud-init.

one-command installer (via cloud-init or by hand).

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.


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:

  1. terraform output public_ip → create the DNS A record for your domain pointing at it.
  2. Confirm the redirect URI https://<domain>/auth/callback is on your Entra ID app registration.
  3. Watch bring-up: ssh azureuser@<ip> sudo tail -f /var/log/uscp-bootstrap.log.
  4. 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.