Installing USCP on AWS
Three supported paths, from simplest to most scalable. Read README.md first for DNS, OIDC, and licensing prerequisites.
- Path A — Terraform (recommended) — one
terraform apply
provisions an EC2 instance, security group, and runs the installer via cloud-init.
- Path B — Manual EC2 + install.sh — full control, launch
an instance yourself and run the one-command installer.
- Path C — EKS + Helm — HA, 3 replicas, autoscaling, external RDS.
For production, use RDS for PostgreSQL instead of the in-instance Docker database — see Using RDS.
Path A — Terraform (recommended)
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, orAWS_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.tfvarscontains your OIDC secret. Keep it out of version control (.gitignore) and preferTF_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.largefor headroom. - Storage: 20 GB gp3 minimum.
- Key pair: select one you hold.
- Security group inbound:
| Port | Source | Why |
|---|---|---|
| 22 (SSH) | your IP/32 | admin |
| 80 (HTTP) | 0.0.0.0/0 | ACME HTTP-01 challenge + redirect to 443 |
| 443 (HTTPS) | 0.0.0.0/0 | the 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):
- Builds the
controlplanebinary from source (CGO_ENABLED=0, static) — or installs
USCP_BINARY if you set it.
- Starts PostgreSQL 16 as a Docker container (default
USCP_DB_MODE=docker). - Runs all migrations.
- Generates a self-signed air-gap license (default
LICENSING_MODE=airgap). - Writes
/etc/uscp/controlplane.env(0600, owned byuscp) with a freshly generated
USCP_DATA_KEY.
- Creates and starts the
controlplane.servicesystemd unit (non-rootuscpuser). - Writes
/etc/caddy/Caddyfileand starts Caddy, which obtains a Let's Encrypt
certificate for your domain.
- Opens 80/443 in
ufw/firewalld. - Verifies
/healthzand/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(orverify-fullwith 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.