🤖 Self-host CARLOS Platform
A single AWS host can run a CARLOS console and your first app. This walkthrough starts with one host and covers the console, DNS and your first app.
Install Git, OpenTofu 1.11 or later (below 2.0), the AWS CLI, and the CARLOS CLI v0.28.15. Authenticate the AWS CLI with an account allowed to provision infrastructure. You also need a domain you control. The download checks below use OpenSSL 3 and curl.
The example uses Debian 13 on an ARM64 host with a t4g.small instance, 2 virtual CPUs, 2 GiB of memory, and a 20 GiB encrypted general-purpose SSD. Size storage and memory for your apps as they grow. You do not need a separate SQL server or container orchestrator.
1. Choose a topology
The public OpenTofu repository provides these examples:
examples/single-hostprovisions one host for a trial.examples/single-regionprovisions two hosts across separate availability zones, which are separate datacentres within one AWS region.examples/two-regionsprovisions two hosts, with one host in each of two AWS regions.
Use at least two hosts for production. These templates provision hosts, but they do not provide automatic high availability or failover. This tutorial follows the single-host trial.
The two-region setup shares one deployment bucket and has one data bucket per region. Deployment metadata remains in the shared bucket. Regional app databases do not make all platform data regional.
Keep separate OpenTofu state for each example. Switching examples against existing state can replace resources.
2. Create the infrastructure repository
Clone the templates and create a private repository for your copy.
git clone https://amadan.net/carlos/opentofu carlos-infrastructure
cd carlos-infrastructure
git checkout --detach 8600447
git switch -c infrastructure
git remote rename origin upstream
git remote add origin YOUR_PRIVATE_REPOSITORY_URL
The checkout is pinned to the template revision used here.
Keep the modules/ and examples/ layout intact. The included .gitignore excludes secrets, variable files, state and saved plans.
State is local by default. Use protected, encrypted shared state for a production team.
Create the deployment signing key on a trusted workstation:
umask 077
carlos release-keygen > deployment-signing.env
Keep this file in your secret manager. Copy only CARLOS_RELEASE_PUBKEY into OpenTofu. The private CARLOS_RELEASE_KEY is for the console; keep it out of Git and OpenTofu state.
3. Provision the host
Copy the single-host variables and edit the account, name, domains, public signing key, and region.
cd examples/single-host
cp terraform.tfvars.example terraform.tfvars
Set values such as:
aws_account_id = "123456789012"
name = "carlos"
apps_domain = "apps.example.com"
release_public_key = "MATCHING_CARLOS_RELEASE_PUBKEY"
region = "eu-west-1"
Validate the configuration and read the plan before applying it:
tofu init
tofu fmt
tofu validate
tofu plan -out=platform.tfplan
tofu apply platform.tfplan
tofu output hosts
tofu output -raw deployment_bucket
tofu output -raw data_bucket
AWS charges begin after resources are created. The plan creates a virtual private cloud, subnets, an internet gateway, the host and its public address, a security group allowing ports 80 and 443 but no SSH, an instance role, and private versioned S3 buckets.
Host setup installs CARLOS and AWS Systems Manager, creates the carlos user, and starts the edge service.
Run the host commands below through AWS Systems Manager Run Command, using AWS-RunShellScript. They run as root.
Wait for cloud-init and check the edge service:
cloud-init status --wait
systemctl is-active carlos-edge
Use your secret manager’s secure delivery method for the private signing key. Keep it out of Run Command requests and logs.
4. Install the public console
Install OpenSSL 3 on the host if it is missing, then download and verify the ARM64 console:
set -eu
install_dir="$(mktemp -d /var/lib/carlos-console-install.XXXXXX)"
chmod 0700 "$install_dir"
trap 'rm -rf "$install_dir"' EXIT
cd "$install_dir"
release_url=https://github.com/carlosframework/releases/releases/download/v0.28.15
curl -fSLO "$release_url/carlos-console-linux-arm64"
curl -fSLO "$release_url/CONSOLE_SHA256SUMS.txt"
curl -fSLO "$release_url/CONSOLE_SHA256SUMS.txt.sig"
printf '\060\052\060\005\006\003\053\145\160\003\041\000' > public.der
printf '%s' 'R+EO3qQ47VsuKl9Qq/pkDXBucPYvzp5lU2ut4zpnJPc=' | base64 -d >> public.der
base64 -d CONSOLE_SHA256SUMS.txt.sig > signature.bin
openssl pkeyutl -verify -pubin -inkey public.der -keyform DER \
-rawin -in CONSOLE_SHA256SUMS.txt -sigfile signature.bin
sha256sum -c --ignore-missing CONSOLE_SHA256SUMS.txt
install -d -m 0755 /opt/carlos
install -m0755 carlos-console-linux-arm64 /opt/carlos/console
This verifies CARLOS’s release signature, then the binary’s checksum. The verification key is different from your deployment signing key. Do not install the binary if either check fails.
5. Configure and start the console
Create the console environment file before adding its contents, so it is private from the start:
install -o root -g root -m 0600 /dev/null /etc/carlos/console.env
Write the following non-secret configuration into /etc/carlos/console.env. Replace the bucket and public-key placeholders with your values. Add the private signing key separately, as described below.
AWS_REGION=eu-west-1
CARLOS_DEPLOYMENT_BUCKET=DEPLOYMENT_BUCKET_OUTPUT
CARLOS_RELEASE_PUBKEY=MATCHING_CARLOS_RELEASE_PUBKEY
CARLOS_REGION=primary
CARLOS_HIBERNATE_BUCKET=DATA_BUCKET_OUTPUT
CARLOS_HIBERNATE_REGION=eu-west-1
CARLOS_HIBERNATE_PREFIX=instances
CONSOLE_ORIGIN=https://console.example.com
CONSOLE_ADDR=127.0.0.1:8090
CONSOLE_DB=/data/console/console.db
CARLOS_REGISTRY=/data/registry.db
PLATFORM_APPS_DOMAIN=apps.example.com
CONSOLE_HOME_REGION=primary
CONSOLE_RESERVE_PLATFORM_DOMAINS=true
The console-only file is read by the system service and is not shared with app processes. Do not add AWS static keys. The host role provides AWS access.
install -d -o carlos -g carlos -m0750 /data/console
chown root:root /etc/carlos/console.env
chmod 0600 /etc/carlos/console.env
To enter the private key, install the Session Manager plugin on your workstation, then open an interactive session using the instance ID from tofu output hosts:
aws ssm start-session --region eu-west-1 --target INSTANCE_ID
In that remote session, open a root Bash shell using the instance’s SSM administrative access:
sudo /bin/bash
Run the following line, then paste only the private key value at the prompt. Input is hidden using the AWS-recommended approach for sensitive session input. Retrieve the value from your secret manager; do not paste it as a shell command.
set +x; IFS= read -r -s -p "Private release key: " release_seed && test -n "$release_seed" && printf "CARLOS_RELEASE_KEY=%s\n" "$release_seed" >> /etc/carlos/console.env; unset release_seed; printf "\n"
Do this once on a fresh configuration file, then exit the interactive shell and session. The key stays in the root-owned file; it is not sent in a Run Command request.
Create /etc/systemd/system/console.service:
[Unit]
Description=CARLOS console
After=network-online.target carlos-edge.service
Wants=network-online.target
[Service]
User=carlos
Group=carlos
EnvironmentFile=/etc/carlos/console.env
ExecStart=/opt/carlos/console serve
Restart=on-failure
RestartSec=2
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
ReadWritePaths=/data /run/carlos
PrivateTmp=yes
[Install]
WantedBy=multi-user.target
Start the console:
systemctl daemon-reload
systemctl enable --now console
curl -fsS http://127.0.0.1:8090/healthz
The response should be ok v0.28.15. The health endpoint alone does not confirm that promotions are enabled: the console also starts without a signing key. Check the service log for configuration errors and for “promotions disabled” before deploying. Grant the first operator before external sign-in:
runuser -u carlos -- /opt/carlos/console grant --operator YOU@example.com
Use the email address you will sign in with through Keymail. No OAuth client registration is needed.
6. Add the console route and DNS records
Register the console with the edge service:
runuser -u carlos -- /opt/carlos/carlos add \
--app console \
--kind service \
--addr 127.0.0.1:8090 \
--unit console.service \
console.example.com
Create these DNS records:
console.example.compoints to the host’s public address.*.apps.example.compoints to the same address.
Use DNS-only A records and the public address from tofu output hosts.
The edge service obtains a certificate for each hostname through HTTP-01 validation. Ports 80 and 443 must be reachable from the internet. The templates do not configure automatic wildcard certificates through Route 53.
Check the public console:
curl -fsS https://console.example.com/healthz
Open the console in a browser and sign in with Keymail. Follow the onboarding steps to create your first account. Do not expose port 8090.
7. Deploy your first app
On your workstation, point the CARLOS CLI at your console:
carlos auth login --console https://console.example.com
carlos auth default --project https://console.example.com
carlos auth whoami
Follow the hand-built app instructions and the CARLOS CLI guide. Use your console and app domain in every example. A Notes host would look like notes.YOUR_ACCOUNT_ID.apps.example.com, not a carloku.com host.
Build the Notes sample with Go 1.26 or later.
After deployment, check the URL printed by the CLI and confirm the response includes the expected X-Carlos-Version value.
For apps that need secrets, run carlos secrets genkey on your trusted workstation. Put CARLOS_SECRETS_KEY in /etc/carlos/host.env on each host and CARLOS_SECRETS_PUBKEY in /etc/carlos/console.env. Keep both files root-owned with mode 0600, then run systemctl restart carlos-edge console on the host. These sealing keys are a separate pair from your release signing keys; keep the private key out of Git and OpenTofu state.
8. Prepare before customers use it
This single-host walkthrough ends with a trial installation. The templates do not install a standalone Litestream service for the console or its always-on database, so do not treat the console database as backed up.
Configure continuous backups for /data/console/console.db and /data/registry.db. Test a restore. Protect the deployment signing key and any sealing keys separately.
CARLOS replicates hibernating app state through the platform, but that does not replace console backups. Before customer use, add spare host capacity, traffic steering with health checks, a host-failure test, and protected shared state.
Outside AWS
Hosts outside AWS can run CARLOS. An S3-compatible endpoint changes the storage endpoint only. Email through SES, managed storage access through IAM, and latency-based DNS through Route 53 remain AWS integrations. This guide does not cover a deployment without AWS services.