Deploy on AWS EC2
This guide walks you through deploying EnclaveStation on an AWS EC2 instance. EC2 gives you more control over instance types, networking, and storage compared to Lightsail, but requires a bit more configuration.
Estimated time: 25–35 minutes
What you'll need
- An AWS account — create one here if you don't have one
- A domain name (optional, but required for HTTPS)
- A local terminal with an SSH client
Architecture overview
EnclaveStation runs as three Docker containers orchestrated by Docker Compose:
Internet ──► Nginx (:80/:443)
├── / → React SPA (static files)
├── /api/* → C++ backend (:9001)
└── /ws → WebSocket to backend (:9001)
Backend (:9001) ──► PostgreSQL (:5432)Everything runs on a single EC2 instance.
Step 1: Launch an EC2 instance
Open the EC2 console
Click Launch instances
Configure the instance:
Setting Value Name enclave-stationAMI Ubuntu Server 24.04 LTS (64-bit x86) Instance type See table below Key pair Create a new key pair or select an existing one Instance type recommendations:
Type vCPUs RAM Best for Cost (approx.) t3.micro2 1 GB 1–10 users, light usage ~$8/mo t3.small2 2 GB 10–50 users, recommended ~$15/mo t3.medium2 4 GB 50+ users or heavy file uploads ~$30/mo Free tier
If your AWS account is less than 12 months old,
t2.micro(1 vCPU, 1 GB RAM) is free-tier eligible — enough for trying things out with a few users.Under Network settings, click Edit and configure:
- Auto-assign public IP: Enable
- Under Security group rules, ensure you have:
- SSH (port 22) from your IP
- HTTP (port 80) from anywhere (
0.0.0.0/0) - HTTPS (port 443) from anywhere (
0.0.0.0/0)
Under Configure storage, set the root volume to at least 20 GB (gp3)
Click Launch instance
WARNING
Do not open port 5432 (PostgreSQL) or 9001 (backend). These should only be accessible internally between containers.
Step 2: Allocate an Elastic IP
A public IP assigned at launch can change if the instance is stopped. An Elastic IP is free while attached to a running instance.
- In the EC2 console, go to Elastic IPs (under Network & Security)
- Click Allocate Elastic IP address → Allocate
- Select the new Elastic IP → Actions → Associate Elastic IP address
- Select your
enclave-stationinstance → Associate
Note down the Elastic IP — you'll need it for DNS.
Step 3: Connect via SSH
chmod 400 ~/Downloads/your-key-pair.pem
ssh -i ~/Downloads/your-key-pair.pem ubuntu@YOUR_ELASTIC_IPStep 4: Install Docker
# Update system packages
sudo apt update && sudo apt upgrade -y
# Install Docker using the official convenience script
curl -fsSL https://get.docker.com | sudo sh
# Add your user to the docker group
sudo usermod -aG docker $USER
# Apply group change
newgrp docker
# Verify
docker --version
docker compose versionStep 5: Clone and configure
# Clone the repository with submodules
git clone --recurse-submodules https://github.com/dariusjlukas/enclave-station.git
cd enclave-station
# Create your environment file from the example
cp .env.example .envEdit the .env file:
nano .env# IMPORTANT: Change this to a strong, unique password
POSTGRES_PASSWORD=your-secure-password-here
# Set this to your domain or Elastic IP
PUBLIC_URL=https://chat.example.comGenerating a strong password
openssl rand -base64 24Step 6: Build and start
docker compose up -d --buildThe first build takes several minutes. Verify everything is running:
docker compose psYou should see three services all showing as running. Visit http://YOUR_ELASTIC_IP to confirm. The first user to register becomes the admin.
Step 7: Set up HTTPS with Let's Encrypt
Point your domain to the server
Create a DNS A record pointing to your Elastic IP:
| Type | Name | Value |
|---|---|---|
| A | chat (or @ for root domain) | YOUR_ELASTIC_IP |
Verify propagation:
dig +short chat.example.comObtain a certificate
# Install certbot
sudo apt install -y certbot
# Stop containers so certbot can use port 80
docker compose down
# Obtain a certificate
sudo certbot certonly --standalone -d chat.example.comConfigure Nginx for SSL
cat > nginx/nginx-ssl.conf << 'NGINX'
server {
listen 80;
server_name _;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name _;
ssl_certificate /etc/letsencrypt/live/chat.example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/chat.example.com/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
root /usr/share/nginx/html;
index index.html;
# API proxy
location /api/ {
proxy_pass http://backend:9001;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
client_max_body_size 0;
proxy_read_timeout 3600;
proxy_send_timeout 3600;
proxy_connect_timeout 60;
}
# WebSocket proxy
location /ws {
proxy_pass http://backend:9001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 86400;
}
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
}
NGINXUpdate the frontend service in docker-compose.yml:
nano docker-compose.ymlReplace the frontend service with:
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
volumes:
- ./nginx/nginx-ssl.conf:/etc/nginx/conf.d/default.conf:z
- /etc/letsencrypt:/etc/letsencrypt:ro
ports:
- "80:80"
- "443:443"
depends_on:
- backendStart everything:
docker compose up -d --buildAutomatic certificate renewal
sudo crontab -eAdd:
0 3 * * * certbot renew --pre-hook "cd /home/ubuntu/enclave-station && docker compose stop frontend" --post-hook "cd /home/ubuntu/enclave-station && docker compose start frontend" >> /var/log/certbot-renew.log 2>&1Updating EnclaveStation
cd ~/enclave-station
git pull --recurse-submodules
docker compose up -d --buildBackups
Database
docker compose exec postgres pg_dump -U chatapp chatapp > backup-$(date +%Y%m%d).sqlUploaded files
docker cp $(docker compose ps -q backend):/data/uploads ./uploads-backupAutomating backups
mkdir -p ~/backups
sudo crontab -e0 2 * * * cd /home/ubuntu/enclave-station && docker compose exec -T postgres pg_dump -U chatapp chatapp | gzip > /home/ubuntu/backups/db-$(date +\%Y\%m\%d).sql.gz 2>&1Offsite backups with S3
sudo apt install -y awscli
aws s3 sync ~/backups s3://your-backup-bucket/enclave-station/Troubleshooting
Can't connect from the browser
- Check the EC2 Security Group allows inbound traffic on ports 80 and 443
- Verify containers are running:
docker compose ps - Test locally:
curl -I http://localhost
Instance runs out of memory
If the backend or PostgreSQL gets OOM-killed, your instance type may be too small. Check with:
dmesg | grep -i "oom\|killed"Upgrade to a larger instance type via the EC2 console (requires a stop/start).
Disk space issues
df -h
docker system df
docker system prune -aLightsail vs EC2
| Lightsail | EC2 | |
|---|---|---|
| Pricing | Fixed monthly ($5–$160) | Pay-per-hour, variable |
| Networking | Bundled transfer | Separate charges |
| Flexibility | Limited instance types | Full instance catalog |
| Best for | Simple, predictable deployments | Custom networking, scaling needs |
If you want the simplest setup with predictable costs, consider deploying on Lightsail instead.
Costs
| Resource | Cost (approx.) |
|---|---|
EC2 t3.small instance | ~$15/mo |
| Elastic IP (attached) | Free |
| 20 GB gp3 storage | ~$1.60/mo |
| Data transfer (first 100 GB/mo) | Free |
| Let's Encrypt SSL | Free |
| Total | ~$17/mo |
Costs vary by region. Use the AWS Pricing Calculator for exact estimates.

