# Deployment Guide This guide covers deploying GcIndexRelay on a VPS using Docker Compose. ## Architecture ``` Host reverse proxy (port 80/443) → localhost:4000 ↓ Phoenix container (app) — published port 4000:4000 ↓ (internal Docker network only) AGE/Postgres container (db) — no published ports, volume: pgdata ``` TLS is terminated by the host-level reverse proxy. The Phoenix app runs behind it and trusts the `X-Forwarded-Proto` header to enforce HTTPS. ## Prerequisites - A VPS running Debian/Ubuntu - Docker and Docker Compose installed - A domain name pointed at your VPS ## 1. Install Docker and Docker Compose ```bash curl -fsSL https://get.docker.com | sh sudo usermod -aG docker $USER # Log out and back in for group membership to take effect ``` Verify: `docker compose version` ## 2. Clone the Repository ```bash git clone /opt/gc_index_relay cd /opt/gc_index_relay ``` ## 3. Configure Secrets Copy the example file and fill in your values: ```bash cp .env.example .env $EDITOR .env ``` Required values to set in `.env`: - **`POSTGRES_PASSWORD`** — use a strong random password - **`SECRET_KEY_BASE`** — generate with: ```bash docker run --rm hexpm/elixir:1.17.3-erlang-27.3.4.7-debian-trixie-20260202-slim \ mix phx.gen.secret # or without Docker: openssl rand -base64 64 ``` - **`PHX_HOST`** — your actual domain name (e.g. `relay.example.com`) ## 4. Set Up the Reverse Proxy The app listens on `localhost:4000`. Configure nginx or Caddy to forward traffic and terminate TLS. ### Caddy (recommended) ```caddy relay.example.com { reverse_proxy localhost:4000 } ``` ### nginx ```nginx server { listen 443 ssl; server_name relay.example.com; ssl_certificate /etc/letsencrypt/live/relay.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/relay.example.com/privkey.pem; location / { proxy_pass http://localhost:4000; proxy_set_header Host $host; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; } } ``` ## 5. Configure Firewall Expose only SSH, HTTP, and HTTPS to the public internet: ```bash sudo ufw allow 22/tcp sudo ufw allow 80/tcp sudo ufw allow 443/tcp sudo ufw enable ``` The database port (5432) must **not** be exposed — it is on an internal Docker network only and never published to the host. ## 6. Build and Start the Stack ```bash docker compose build docker compose up -d ``` On first start, the app container will: 1. Wait for the database to pass its health check 2. Run any pending Ecto migrations 3. Start the Phoenix server ## 7. Verify the Deployment ```bash # Check container status docker compose ps # Health check endpoint curl http://localhost:4000/health # Expected: 200 OK, body: "ok" # API endpoint curl http://localhost:4000/api/events # Expected: JSON response # Confirm database port is NOT accessible from host curl http://localhost:5432 # Expected: connection refused ``` ## Operations ### View logs ```bash docker compose logs -f app docker compose logs -f db ``` ### Restart the app ```bash docker compose restart app ``` ### Stop everything ```bash docker compose down ``` Database data is persisted in the `pgdata` Docker volume and survives `docker compose down`. To also delete the data: ```bash docker compose down -v ``` ### Update to a new version ```bash git pull docker compose build docker compose up -d ``` Migrations run automatically on startup.