Set up on AWS EC2
This guide runs Lumaft as a container on one EC2 instance. The instance profile supplies read-only S3 access, an EBS volume holds the SQLite database, and a reverse proxy on the instance terminates TLS.
It is the simplest AWS deployment and the easiest to reason about: one instance, one volume, one container.
Architecture
Internet ──443──▶ ┌──────────────────────────────────────┐
│ EC2 instance │
│ ┌──────────┐ ┌────────────────┐ │
│ │ Caddy / │ │ lumaft │ │──── S3 (state bucket)
│ │ nginx │──▶ │ 127.0.0.1:8080 │ │──── ghcr.io (image pull)
│ │ :443 │ │ /data ← EBS │ │──── licensing service
│ └──────────┘ └────────────────┘ │
└──────────────────────────────────────┘
Before you begin
- An AWS account and the AWS CLI configured on your workstation.
- A hosted zone and a hostname for Lumaft, for example
lumaft.example.com. - The verified image digest. See Deployment and setup.
- Your backend details and the backends JSON document. See Find your backend details and Connect a backend.
Deploy with Pulumi creates the instance, role, volume, and security group from one program if you prefer not to click through the console.
Step 1: Security group
| Direction | Protocol | Port | Source / destination | Purpose |
|---|---|---|---|---|
| Inbound | TCP | 443 | Your allowed CIDR ranges | Browser and runner traffic |
| Inbound | TCP | 80 | 0.0.0.0/0 (optional) |
ACME HTTP-01 challenges only |
| Outbound | TCP | 443 | 0.0.0.0/0 |
S3, registry, licensing, ACME |
Do not open port 8080. Lumaft binds to the loopback interface and only the reverse proxy
reaches it.
Do not open port 22. Use AWS Systems Manager Session Manager for shell access; it needs no
inbound rule.
Step 2: IAM instance profile
Create a role for EC2 with two policies:
AmazonSSMManagedInstanceCorefor Session Manager.- The read-only state policy below.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ListPulumiState",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::BUCKET_NAME",
"Condition": { "StringLike": { "s3:prefix": ["OPTIONAL_PREFIX/.pulumi/*"] } }
},
{
"Sid": "ReadPulumiState",
"Effect": "Allow",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::BUCKET_NAME/OPTIONAL_PREFIX/.pulumi/*"
}
]
}
Attach the role as the instance profile. Lumaft picks it up through the AWS SDK credential chain with no configuration. Require IMDSv2 on the instance. The CLI commands that create the policy, the role, and the instance profile are in IAM setup on AWS.
Step 3: Launch the instance
- AMI: Amazon Linux 2023 or Ubuntu LTS.
- Instance type: start at 2 vCPU / 4 GiB (for example
t3.mediumort4g.medium), which leaves room for the proxy beside Lumaft's 1 vCPU / 2 GiB baseline. - Root volume: the AMI default.
- Data volume: add a second, encrypted gp3 EBS volume (50 GiB is a comfortable start). Set Delete on termination to No so the database outlives the instance.
- Subnet: a public subnet with an Elastic IP, or a private subnet behind an ALB (see Alternative: ALB in front).
- Metadata: IMDSv2 required.
Connect with Session Manager:
aws ssm start-session --target i-0123456789abcdef0
Install Docker
Amazon Linux 2023:
sudo dnf install -y docker
sudo systemctl enable --now docker
Ubuntu:
sudo apt-get update && sudo apt-get install -y docker.io
sudo systemctl enable --now docker
Step 4: Prepare the data volume
Find the data volume's device name first; it is not always nvme1n1:
lsblk
The unformatted device with no mount point and the size you chose is the data volume. Then:
DEVICE=/dev/nvme1n1 # from lsblk
sudo mkfs.ext4 -L lumaft-data "$DEVICE"
sudo mkdir -p /srv/lumaft/data
echo 'LABEL=lumaft-data /srv/lumaft/data ext4 defaults,nofail 0 2' | sudo tee -a /etc/fstab
sudo mount -a
sudo chown 1000:1000 /srv/lumaft/data
sudo chmod 700 /srv/lumaft/data
UID 1000 is the node user inside the container. The directory must be owned by it and not
writable by anyone else. mkfs erases the device — run it once, on the new volume only.
Step 5: Prepare the secret files
sudo mkdir -p /srv/lumaft/run
sudo chmod 700 /srv/lumaft/run
# Bootstrap administrator password (12–4096 characters)
sudo sh -c 'umask 077; printf "%s\n" "$(openssl rand -base64 24)" > /srv/lumaft/run/admin-password'
# Backend definitions — see Connect a backend
sudo sh -c 'umask 077; cat > /srv/lumaft/run/backends.json' <<'JSON'
{
"schemaVersion": 1,
"backends": [
{
"kind": "s3",
"id": "production",
"displayName": "Production infrastructure",
"bucket": "BUCKET_NAME",
"prefix": "OPTIONAL_PREFIX",
"layout": "project-scoped",
"region": "us-east-1",
"forcePathStyle": false,
"enabled": true
}
]
}
JSON
sudo chown -R 1000:1000 /srv/lumaft/run
sudo chmod 600 /srv/lumaft/run/*
Record the generated password somewhere protected. You need it once, to sign in and create your own account.
Step 6: Run Lumaft
LUMAFT_IMAGE="ghcr.io/dekglas/lumaft@sha256:<digest>"
sudo docker run --name lumaft \
--detach \
--restart unless-stopped \
--init \
--read-only \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--publish 127.0.0.1:8080:8080 \
--mount type=bind,source=/srv/lumaft/data,target=/data \
--mount type=bind,source=/srv/lumaft/run,target=/run/lumaft,readonly \
--env LUMAFT_LOCAL_ADMIN_PASSWORD_FILE=/run/lumaft/admin-password \
--env LUMAFT_BACKENDS_FILE=/run/lumaft/backends.json \
--env LUMAFT_PUBLIC_ORIGIN=https://lumaft.example.com \
"$LUMAFT_IMAGE"
curl -fsS http://127.0.0.1:8080/api/v1/readiness
--publish 127.0.0.1:8080:8080 is deliberate. The container is reachable only from the instance
itself.
Step 7: Terminate TLS
Point your DNS record at the instance's Elastic IP first; the certificate authority must be able to reach the hostname.
Caddy obtains and renews a certificate automatically. Running it as a container keeps the instance free of extra packages:
sudo mkdir -p /srv/lumaft/caddy
sudo tee /srv/lumaft/caddy/Caddyfile >/dev/null <<'EOF'
lumaft.example.com {
reverse_proxy 127.0.0.1:8080
}
EOF
sudo docker run --name caddy \
--detach \
--restart unless-stopped \
--network host \
--mount type=bind,source=/srv/lumaft/caddy/Caddyfile,target=/etc/caddy/Caddyfile,readonly \
--mount type=volume,source=caddy-data,target=/data \
caddy:2
--network host lets Caddy listen on 443 and reach Lumaft on 127.0.0.1:8080. The
caddy-data volume keeps the certificate across restarts. Replace lumaft.example.com with your
hostname; that line is the only configuration Caddy needs.
With nginx, terminate TLS on 443 and proxy_pass http://127.0.0.1:8080; with
proxy_set_header Host $host;. Either way, browsers only ever connect over HTTPS, so the
default Secure __Host-lumaft-session cookie works. Leave LUMAFT_ALLOW_HTTP unset.
Verify from your workstation:
curl -fsS https://lumaft.example.com/api/v1/readiness
The first request can take up to a minute while Caddy obtains the certificate.
Step 8: First sign-in and cleanup
- Open
https://lumaft.example.comand sign in asadminwith the generated password. - Open Administration → Backends and confirm the backend diagnostic succeeded.
- Stop the container, remove
--env LUMAFT_LOCAL_ADMIN_PASSWORD_FILE=…from the run command, securely delete/srv/lumaft/run/admin-password, and start it again.
Alternative: ALB in front
If you prefer an ALB to terminate TLS:
- Place the instance in a private subnet with a NAT Gateway route for outbound S3 and registry traffic.
- Publish the container on the instance's private address instead of loopback
(
--publish 10.0.1.20:8080:8080) and restrict the security group so port8080admits only the ALB's security group. - Register the instance in a target group (target type Instance, HTTP, port 8080, health
check
/api/v1/readiness).
The ALB then provides the HTTPS origin and no proxy runs on the instance.
Operate on EC2
Backups
Backups are cold. Stop the container, copy the file, start it again:
sudo mkdir -p /srv/lumaft/backups && sudo chmod 700 /srv/lumaft/backups
sudo docker stop -t 30 lumaft
sudo cp /srv/lumaft/data/lumaft.db /srv/lumaft/backups/lumaft-$(date -u +%Y%m%dT%H%M%SZ).db
sudo docker start lumaft
Docker Compose and systemd packages this instance's run command, restart-on-boot, and a backup script so you do not have to retype them.
Copy backups off the instance (for example to a separate, versioned S3 bucket with its own access policy). A crash-consistent EBS snapshot is a useful second layer but is not a verified cold backup.
Restore
sudo docker stop -t 30 lumaft && sudo docker rm lumaft- Replace
/srv/lumaft/data/lumaft.dbwith the backup and delete any leftoverlumaft.db-wal,lumaft.db-shm, andlumaft.db.ownerfiles. - Run the container again and confirm readiness.
Upgrades
- Stop the container cleanly and take a cold backup.
- Run the same command with the new digest against the same
/srv/lumaft/data. - Confirm readiness. If startup refuses because the upgrade carries a gated migration, nothing has been migrated; see Database integration.
Monitoring
Poll https://lumaft.example.com/api/v1/readiness from your monitoring system. For storage
pressure, poll the authenticated /api/v1/storage-health endpoint with an administrator session
and alert on writeProbe.outcome == "failed" or low dataDirectory.freeBytes.
If something does not work, Troubleshooting is organized by symptom.