Production Setup Notes

Production setup notes for ekx. Keep this doc updated when prod paths, networks, or deploy steps change.

Quick Facts

  • Host: 64.227.103.35 (Ubuntu 24.10)
  • Primary domain: joinekx.com (www.joinekx.com)
  • App container: ekx_prod_app (Next.js)
  • Pandapower container: ekx_prod_pandapower
  • Database: ekx_prod_postgres (PostgreSQL 16)
  • Reverse proxy: ekx_reverse_proxy (nginx)

Key Paths (Server)

  • App compose: /var/www/ekx/docker-compose.yml
  • App env file: /var/www/ekx/.env.production
  • Proxy compose: /var/www/ekx/proxy/docker-compose.yml
  • Proxy nginx config: inside container at /etc/nginx/conf.d/*.conf
  • View active config: docker exec ekx_reverse_proxy nginx -T

Networks and Volumes

  • App compose defines networks: app_network and shared_web (external)
  • shared_web must exist for reverse proxy -> app routing
  • Volume: ekx_postgres_data
  • Warning is expected unless marked external:
  • volumes:

postgres_data: external: true name: ekx_postgres_data

Images and Tags

  • GHCR images:
  • ghcr.io/bwgibb/ekx/app
  • ghcr.io/bwgibb/ekx/pandapower
  • ghcr.io/bwgibb/ekx/pdf
  • ghcr.io/bwgibb/ekx/migrator
  • Local tags used by compose:
  • ekx-app-prod:latest
  • ekx-pandapower-prod:latest
  • ekx-migrator:latest

Note: pdf image is built/pulled but not run in prod compose.

Deploy Flow (GitHub Actions)

Workflow: .github/workflows/build-deploy.yml

1) Build and push images to GHCR. 2) SSH to prod. 3) Pull by digest, tag local :latest. 4) Run migrations in migrator container. 5) Recreate app + pandapower with compose (not docker restart).

The workflow uses:

  • COMPOSE_FILE_PATH=/var/www/ekx/docker-compose.yml
  • env file preference: /var/www/ekx/.env.production (fallback: .env)

Manual Deploy (Server)

Recreate services with the latest pulled/tagged images:

# Ensure shared network exists
docker network inspect shared_web >/dev/null 2>&1 || docker network create shared_web

# Recreate app + pandapower
docker compose -f /var/www/ekx/docker-compose.yml --env-file /var/www/ekx/.env.production \
  up -d --no-build --force-recreate --no-deps pandapower app

Verify container image ID matches local tag:

docker image inspect ekx-app-prod:latest --format '{{.Id}}'
docker inspect ekx_prod_app --format '{{.Image}}'

Common Issues

502 Bad Gateway after deploy

Cause: nginx cached old container IP for ekx_prod_app.

Fix:

# Reload to re-resolve docker DNS
docker exec ekx_reverse_proxy nginx -s reload

# If still 502, restart proxy
docker restart ekx_reverse_proxy

Check upstream connectivity from the proxy container:

docker exec ekx_reverse_proxy wget -qO- http://ekx_prod_app:3000/ || echo "proxy cannot reach app"

Migrations fail with "type already exists"

Drizzle migration attempted to create an enum/type that already exists. This does not block container recreation, but should be fixed by making migrations idempotent or reconciling the migrations table with DB state.

Compose warnings about missing env vars

Make sure /var/www/ekx/.env.production is present and includes required keys:

  • DATABASE_URL
  • AUTH_SECRET
  • AUTH_GOOGLE_ID / AUTH_GOOGLE_SECRET
  • Stripe, Turnstile, Resend, etc.

Stripe Billing Product IDs

Production Stripe account: acct_1RpuZEGvbjJF0eeu (Bremen Engineering Inc.).

Canonical public products:

  • Starter: prod_TnxlgGLkt63uvI
  • Monthly: price_1TFyHqGvbjJF0eeuZ0rHZqMo ($27/mo)
  • Yearly: price_1TFyHqGvbjJF0eeuQXjfr2KP ($270/yr)
  • Professional: prod_Tnxl6hGCGjwWXL
  • Monthly: price_1TFyHrGvbjJF0eeuGfdjVmUM ($149/user/mo)
  • Yearly: price_1TFyHsGvbjJF0eeudYSOGmav ($1,490/user/yr)

Team is legacy-only and should remain inactive in Stripe-hosted surfaces:

  • Product: prod_Tnxl0YXxR1eoqf
  • Monthly: price_1SqLpqGvbjJF0eeus6Wy20Gn
  • Yearly: price_1SqLpqGvbjJF0eeunGvapoNe
  • Lifetime: price_1SqLprGvbjJF0eeunwaulrP1

Reverse Proxy Notes

  • Proxy allows only Cloudflare IP ranges (deny all others).
  • Nginx config uses: proxy_pass http://ekx_prod_app:3000;
  • Duplicate server_name warnings indicate multiple server blocks for joinekx.com.

Clean up duplicates to avoid undefined behavior.