# Single-node Tarakan deployment: the Phoenix release plus PostgreSQL. # # From the repo root (or /opt/tarakan after rsync): # # 1. cp .env.example .env && edit it (at minimum SECRET_KEY_BASE, PHX_HOST) # 2. docker compose --project-directory . -f deploy/docker/compose.yml up -d --build # # Production activate path: scripts/deploy/deploy.sh (loads a pre-built image). # Put a TLS-terminating reverse proxy (Caddy, nginx, Traefik) in front for HTTPS. services: db: image: postgres:16 restart: unless-stopped logging: driver: json-file options: max-size: "10m" max-file: "3" environment: POSTGRES_USER: ${POSTGRES_USER:?set POSTGRES_USER in .env} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env} POSTGRES_DB: ${POSTGRES_DB:-tarakan} volumes: - db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"] interval: 5s timeout: 5s retries: 10 app: image: ${APP_IMAGE:-tarakan-app} build: context: ../.. dockerfile: deploy/docker/Dockerfile restart: unless-stopped logging: driver: json-file options: max-size: "10m" max-file: "3" security_opt: - no-new-privileges:true # Erlang sizes its port table from RLIMIT_NOFILE. Some Docker hosts expose # a billion-file limit, which wastes more than 1 GB before serving traffic. ulimits: nofile: soft: 65536 hard: 65536 depends_on: db: condition: service_healthy environment: # Required. DATABASE_URL: ecto://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db/${POSTGRES_DB:-tarakan} # Same Docker network as Postgres — no TLS between containers. DATABASE_SSL: ${DATABASE_SSL:-false} SECRET_KEY_BASE: ${SECRET_KEY_BASE:?set SECRET_KEY_BASE in .env (mix phx.gen.secret)} PHX_HOST: ${PHX_HOST:-localhost} PORT: ${PORT:-4000} # Keep the default pool small enough for the minimum 2 GB deployment. POOL_SIZE: ${POOL_SIZE:-5} # When a reverse proxy sits in front, list its Docker/host CIDRs so client # IPs and rate limits are correct (e.g. 172.16.0.0/12,127.0.0.1). TRUSTED_PROXIES: ${TRUSTED_PROXIES:-} # Persistent repository storage (see volumes below). MIRROR_DIR: /app/storage/mirrors HOSTED_DIR: /app/storage/hosted # Git over SSH. Off unless GIT_SSH_ENABLED is true|1. The host key lives # on its own volume: it is the server's identity, so regenerating it # makes every client that has ever connected refuse with # "REMOTE HOST IDENTIFICATION HAS CHANGED". GIT_SSH_ENABLED: ${GIT_SSH_ENABLED:-false} GIT_SSH_PORT: ${GIT_SSH_PORT:-2222} GIT_SSH_HOST_KEY_DIR: /app/storage/ssh # Recommended in production: authenticated GitHub API (5k req/hr, bulk sync). GITHUB_TOKEN: ${GITHUB_TOKEN:-} # Optional OAuth sign-in. GITHUB_CLIENT_ID: ${GITHUB_CLIENT_ID:-} GITHUB_CLIENT_SECRET: ${GITHUB_CLIENT_SECRET:-} # Transactional email through Elektrine's scoped external API. ELEKTRINE_EMAIL_API_URL: ${ELEKTRINE_EMAIL_API_URL:-https://elektrine.com} ELEKTRINE_EMAIL_API_KEY: ${ELEKTRINE_EMAIL_API_KEY:?set ELEKTRINE_EMAIL_API_KEY in .env} # Migrate, then serve. Both boot the release; migrate exits first. command: ["/bin/sh", "-c", "/app/bin/migrate && /app/bin/server"] ports: # The TLS proxy runs on the host. Do not expose Phoenix directly. - "${BIND_IP:-127.0.0.1}:${PORT:-4000}:${PORT:-4000}" # SSH speaks its own transport, so unlike HTTP it is published directly # rather than proxied. Bound to all interfaces by default because a git # remote has to reach it from outside the host; set GIT_SSH_BIND_IP to # narrow it. Harmless while GIT_SSH_ENABLED is false - nothing listens. - "${GIT_SSH_BIND_IP:-0.0.0.0}:${GIT_SSH_PORT:-2222}:${GIT_SSH_PORT:-2222}" volumes: # hosted = source of truth for Tarakan-hosted repos. BACK THIS UP. - hosted:/app/storage/hosted # mirrors = regenerable cache of remote repos (rebuilt from upstream on a # miss). Safe to exclude from backups and to wipe. - mirrors:/app/storage/mirrors # ssh = the daemon's host key. Small, irreplaceable, BACK THIS UP: losing # it is indistinguishable from a man-in-the-middle to every existing # client, and they will refuse to connect until the user clears it. - ssh:/app/storage/ssh volumes: db_data: hosted: mirrors: ssh: