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

Installing USCP on AWS

Three supported paths, from simplest to most scalable. Read README.md first for DNS, OIDC, and licensing prerequisites.

provisions an EC2 instance, security group, and runs the installer via cloud-init.

an instance yourself and run the one-command installer.

For production, use RDS for PostgreSQL instead of the in-instance Docker database — see Using RDS.


The Terraform module in deploy/terraform/ provisions everything: an Ubuntu 22.04 EC2 instance, a locked-down security group (22/80/443), and a cloud-init user_data script that installs Go, clones the repo, and runs deploy/install.sh unattended.

Prerequisites

  • Terraform ≥ 1.5 and AWS CLI configured (aws configure, or AWS_PROFILE).
  • An EC2 key pair in the target region (for SSH). Note its name.
  • Your public IP for SSH lockdown: curl -s https://checkip.amazonaws.com.
  • Your DNS zone reachable so you can add an A record after apply.

Step 1 — Get the code

git clone https://github.com/cfssay/doublelogic_apex uscp && cd uscp/codes/deploy/terraform

Step 2 — Create terraform.tfvars

region             = "us-east-1"
instance_type      = "t3.medium"                 # 2 vCPU / 4 GB — the minimum
key_name           = "my-ec2-keypair"            # an existing EC2 key pair in this region
ssh_ingress_cidr   = "203.0.113.4/32"            # YOUR ip/32 — never 0.0.0.0/0

domain             = "uscp.example.com"
oidc_issuer        = "https://accounts.google.com"
oidc_client_id     = "1234567890-abcdef.apps.googleusercontent.com"
oidc_client_secret = "GOCSPX-xxxxxxxxxxxxxxxxxxxx"
acme_email         = "ops@example.com"

# Optional — override the source repo/toolchain:
# repo       = "https://github.com/cfssay/doublelogic_apex"
# go_version = "1.26.4"
# Optional — bring-your-own security data lake:
# byo_lake_url   = "https://lake.example.com/query"
# byo_lake_token = "..."

All variables (region, instance_type, key_name, ssh_ingress_cidr, domain, oidc_issuer, oidc_client_id, oidc_client_secret, acme_email, repo, go_version, byo_lake_url, byo_lake_token) are defined in variables.tf.

Secrets note: terraform.tfvars contains your OIDC secret. Keep it out of version control (.gitignore) and prefer TF_VAR_oidc_client_secret=... from your secrets manager in CI.

Step 3 — Apply

terraform init
terraform plan      # review: 1 instance, 1 security group
terraform apply     # type 'yes'

Terraform prints the instance's public IP:

Outputs:
public_ip = "54.x.x.x"

Step 4 — Point DNS at it

Create an A record: uscp.example.com → 54.x.x.x. Wait for it to resolve:

dig +short uscp.example.com    # should return 54.x.x.x

TLS issuance needs the name to resolve before cloud-init reaches the Caddy step. If you created the record after apply, cloud-init retries ACME automatically, but you can force it — see Step 5.

Step 5 — Watch the install finish

cloud-init runs install.sh on first boot. SSH in and tail it:

ssh -i ~/.ssh/my-ec2-keypair.pem ubuntu@54.x.x.x
sudo tail -f /var/log/cloud-init-output.log      # watch build → migrate → license → verify

The install is done when you see the installer's /healthz and /readyz checks pass. If ACME failed because DNS wasn't ready, re-run once the record resolves:

sudo systemctl restart caddy

Step 6 — Verify and log in

curl -fsS https://uscp.example.com/healthz    # 200 ok
curl -fsS https://uscp.example.com/readyz     # 200 once DB + license ready

Open https://uscp.example.com and continue with post-install.md.

Destroying

terraform destroy

Path B — Manual EC2 + install.sh

Use this when you want to choose the AMI, attach specific IAM roles, or integrate with an existing VPC.

Step 1 — Launch an instance

  • AMI: Ubuntu Server 22.04 LTS (x86_64) or Amazon Linux 2023.
  • Type: t3.medium (2 vCPU / 4 GB) minimum; t3.large for headroom.
  • Storage: 20 GB gp3 minimum.
  • Key pair: select one you hold.
  • Security group inbound:
Port Source Why
22 (SSH)your IP/32admin
80 (HTTP)0.0.0.0/0ACME HTTP-01 challenge + redirect to 443
443 (HTTPS)0.0.0.0/0the console

Do not expose 8080 or 5432 publicly.

Step 2 — DNS

Create A uscp.example.com → <instance public IP> and confirm with dig +short uscp.example.com.

Step 3 — Prepare the host

ssh -i ~/.ssh/my-key.pem ubuntu@<ip>
sudo apt-get update && sudo apt-get install -y git docker.io
sudo systemctl enable --now docker

(Go is installed by the installer if you build from source; Docker is only needed for the default USCP_DB_MODE=docker database.)

Step 4 — Clone and run the installer

git clone https://github.com/cfssay/doublelogic_apex uscp
cd uscp/codes

sudo \
  USCP_DOMAIN=uscp.example.com \
  USCP_OIDC_ISSUER=https://accounts.google.com \
  USCP_OIDC_CLIENT_ID=1234567890-abcdef.apps.googleusercontent.com \
  USCP_OIDC_CLIENT_SECRET=GOCSPX-xxxxxxxxxxxxxxxxxxxx \
  USCP_ACME_EMAIL=ops@example.com \
  ./deploy/install.sh

What this does, in order (all from deploy/install.sh):

  1. Builds the controlplane binary from source (CGO_ENABLED=0, static) — or installs

USCP_BINARY if you set it.

  1. Starts PostgreSQL 16 as a Docker container (default USCP_DB_MODE=docker).
  2. Runs all migrations.
  3. Generates a self-signed air-gap license (default LICENSING_MODE=airgap).
  4. Writes /etc/uscp/controlplane.env (0600, owned by uscp) with a freshly generated

USCP_DATA_KEY.

  1. Creates and starts the controlplane.service systemd unit (non-root uscp user).
  2. Writes /etc/caddy/Caddyfile and starts Caddy, which obtains a Let's Encrypt

certificate for your domain.

  1. Opens 80/443 in ufw/firewalld.
  2. Verifies /healthz and /readyz.

Step 5 — Verify and log in

curl -fsS https://uscp.example.com/healthz
curl -fsS https://uscp.example.com/readyz

Then first login.

Using RDS for PostgreSQL

For production, don't run the database inside the instance. Provision RDS for PostgreSQL 16 and point the installer at it:

sudo \
  USCP_DOMAIN=uscp.example.com \
  USCP_OIDC_ISSUER=https://accounts.google.com \
  USCP_OIDC_CLIENT_ID=... USCP_OIDC_CLIENT_SECRET=... \
  USCP_ACME_EMAIL=ops@example.com \
  USCP_DB_MODE=external \
  DATABASE_URL='postgres://uscp_app:PASSWORD@mydb.abc123.us-east-1.rds.amazonaws.com:5432/uscp?sslmode=require' \
  ./deploy/install.sh
  • Create the database and a non-superuser role (uscp_app) — this matters: USCP relies on

PostgreSQL FORCE ROW LEVEL SECURITY for per-tenant isolation, and a superuser bypasses RLS. Never point DATABASE_URL at the RDS master/superuser account in production. See post-install.md → Database hardening.

  • Put RDS in a private subnet; allow 5432 only from the EC2 security group.
  • Use sslmode=require (or verify-full with the RDS CA bundle).

Path C — EKS + Helm

For horizontal scale (3+ replicas, HPA, PodDisruptionBudget). The chart is in deploy/helm/uscp/. Full chart reference is in kubernetes.md; the AWS-specific steps are below.

Step 1 — Cluster, database, and image

  • Cluster: an EKS cluster (eksctl create cluster ... or Terraform) with an ingress

controller. The AWS Load Balancer Controller (ALB) or an NGINX ingress both work.

  • Database: RDS for PostgreSQL 16 in the cluster VPC, reachable from the node subnets,

with a non-superuser uscp_app role (see the RDS note above).

  • Image: build and push the container to ECR (the chart's default image

ghcr.io/doublelogic/uscp/controlplane is a placeholder — build your own):

  cd codes
  aws ecr create-repository --repository-name uscp/controlplane
  ACCOUNT=$(aws sts get-caller-identity --query Account --output text)
  REG=$ACCOUNT.dkr.ecr.us-east-1.amazonaws.com
  aws ecr get-login-password | docker login --username AWS --password-stdin $REG
  docker build -t $REG/uscp/controlplane:v1 .        # uses ./Dockerfile
  docker push $REG/uscp/controlplane:v1

Step 2 — Create the secret

The chart reads all secrets from an existing Kubernetes Secret named uscp-secrets:

kubectl create namespace uscp
kubectl -n uscp create secret generic uscp-secrets \
  --from-literal=DATABASE_URL='postgres://uscp_app:PASSWORD@mydb.abc123.us-east-1.rds.amazonaws.com:5432/uscp?sslmode=require' \
  --from-literal=USCP_OIDC_ISSUER='https://accounts.google.com' \
  --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

(Generate the license bundle with make dev-license — see airgap.md — and mount it, or switch to online mode.)

Step 3 — Install the chart

helm install uscp deploy/helm/uscp \
  --namespace uscp \
  --set image.repository=$REG/uscp/controlplane \
  --set image.tag=v1 \
  --set env.USCP_REGION=us-east-1 \
  --set env.USCP_PUBLIC_URL=https://uscp.example.com

Step 4 — Ingress + TLS + DNS

Expose the uscp Service (port 8080) through your ingress with a certificate (ACM on ALB, or cert-manager on NGINX). Point uscp.example.com at the load balancer, then verify:

curl -fsS https://uscp.example.com/readyz

Continue at kubernetes.md for HPA/PDB tuning and post-install.md for first login.