Skip to content

Deployment Guide (Non-Production)

Single EC2 instance running the full Floh stack via Docker Compose with Caddy for automatic HTTPS.

Architecture

Internet
  ├─ :443 ──► Caddy (TLS termination, Let's Encrypt)
  │             ├─ floh-dev.example.com/api/*  ──► server:3000
  │             ├─ floh-dev.example.com/*       ──► web:8080 (nginx)
  │             ├─ portal.example.com/api/*     ──► portal-bff:3001
  │             └─ portal.example.com/*          ──► portal-web:8080 (nginx)
  └─ :8025 ──► MailHog UI (restrict to your IP)

Prerequisites

  • AWS account
  • A domain name with DNS you can control
  • GitHub repo: github.com/AxleResearch/floh

Step 1: Launch EC2 Instance

  • Instance type: t3.medium (2 vCPU, 4 GB RAM)
  • AMI: Ubuntu 24.04 LTS
  • Storage: 30 GB gp3 EBS
  • Key pair: Create or select one (save the .pem file)

Step 2: Security Group

Allow inbound:

Port Protocol Source Purpose
22 TCP Your IP SSH
80 TCP Anywhere HTTP (ACME challenges + redirect)
443 TCP Anywhere HTTPS
8025 TCP Your IP MailHog UI (optional)

Step 3: Elastic IP

Allocate an Elastic IP and associate it with your instance. This gives a stable address that survives reboots.

Step 4: DNS

Create A records pointing to the Elastic IP:

  • floh-dev.example.com<ELASTIC_IP>
  • portal.floh-dev.example.com<ELASTIC_IP>

Caddy will auto-provision Let's Encrypt certificates once DNS resolves.

Step 5: Install Docker

ssh -i your-key.pem ubuntu@<ELASTIC_IP>

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker ubuntu
newgrp docker

docker --version
docker compose version

Step 6: Authenticate Docker to GHCR

Create a GitHub PAT (classic) with the read:packages scope, then on the instance:

echo "<YOUR_PAT>" | docker login ghcr.io -u <YOUR_GITHUB_USERNAME> --password-stdin

Step 7: Create Deployment Directory and Environment

mkdir -p ~/floh && cd ~/floh

Copy .env.deploy.example from the repo and save it as ~/floh/.env. Fill in real values — at minimum:

  • DEPLOY_DOMAIN and DEPLOY_PORTAL_DOMAIN (your actual domains)
  • DB_PASSWORD, JWT_SECRET, SESSION_SECRET (generate strong values)
  • FRONTEND_URL, PORTAL_FRONTEND_URL, OIDC_REDIRECT_URI (match your domains)

Step 8: GitHub Repository Secrets

In Settings → Secrets → Actions, add:

Secret Value
DEPLOY_SSH_KEY Contents of the .pem private key file
DEPLOY_HOST The Elastic IP address
DEPLOY_USER ubuntu

Deploying

  1. Go to Actions → Deploy
  2. Click Run workflow
  3. Optionally specify a branch or tag (defaults to main)
  4. The workflow builds all 4 images, pushes to GHCR, and deploys to the EC2 instance

First deploy takes ~5 minutes (image pulls). Subsequent deploys are faster due to layer caching.

Operations

View logs

ssh -i your-key.pem ubuntu@<ELASTIC_IP>
cd ~/floh
docker compose -f docker-compose.deploy.yml logs -f          # all services
docker compose -f docker-compose.deploy.yml logs -f server    # single service

Restart a service

docker compose -f docker-compose.deploy.yml restart server

Update environment variables

vim ~/floh/.env
docker compose -f docker-compose.deploy.yml up -d   # picks up .env changes

Check service status

docker compose -f docker-compose.deploy.yml ps

Access MailHog

Open http://<ELASTIC_IP>:8025 in your browser (if port 8025 is open in the security group).

Database access

docker compose -f docker-compose.deploy.yml exec postgres psql -U floh -d floh

Changing Domains Later

  1. Update DNS A records for the new domain
  2. Edit ~/floh/.env — update DEPLOY_DOMAIN, DEPLOY_PORTAL_DOMAIN, FRONTEND_URL, PORTAL_FRONTEND_URL, OIDC_REDIRECT_URI, and ALLOWED_PORTAL_ORIGINS
  3. Run docker compose -f docker-compose.deploy.yml up -d to restart with new domains
  4. Caddy auto-provisions new Let's Encrypt certificates

Cost Estimate

Resource Monthly Cost
EC2 t3.medium ~$33
EBS 30 GB gp3 ~$2.50
Elastic IP (attached) Free
Data transfer ~$1-5
GHCR Free (included in GitHub plan)
Total ~$37-41