docker-compose.selfhost.yml
Rama main · réplica de lectura del repositorio original.
# =============================================================================
# GitTerm — self-hosted stack
# =============================================================================
# Runs the full platform (Postgres, Redis, API server, listener, worker, web,
# and the Caddy edge proxy) from the published GHCR images.
#
# ROUTING_MODE=path (default) serves everything from one origin, no DNS setup.
# ROUTING_MODE=subdomain serves workspaces at <port>-<workspace>.<PUBLIC_HOST>
# and needs wildcard DNS (*.PUBLIC_HOST) plus a wildcard TLS certificate on
# whatever terminates TLS in front of the proxy service.
#
# cp .env.selfhost.example .env.selfhost # then fill in the secrets
# docker compose --env-file .env.selfhost -f docker-compose.selfhost.yml up -d
#
# Migrations, catalog seed and admin bootstrap run automatically inside the
# server container on every start (see apps/server/docker-entrypoint.sh).
# Set SKIP_MIGRATIONS/SKIP_SEED/SKIP_ADMIN_SEED=true to manage them yourself.
#
# To build from source instead of pulling images:
# docker compose --env-file .env.selfhost -f docker-compose.selfhost.yml up -d --build
#
# NOTE: docker-compose.yml (no suffix) is the infra-only file used for local
# development against natively-run apps. This file is independent of it.
# =============================================================================
name: gitterm-selfhost
x-internal-env: &internal-env
SERVER_URL: http://server:8080/api
INTERNAL_API_KEY: ${INTERNAL_API_KEY:?INTERNAL_API_KEY is required}
DEPLOYMENT_MODE: self-hosted
NODE_ENV: production
ENCRYPTION_MASTER_KEY: ${ENCRYPTION_MASTER_KEY:?ENCRYPTION_MASTER_KEY is required}
ENCRYPTION_MASTER_KEY_ID: ${ENCRYPTION_MASTER_KEY_ID:-primary}
ENCRYPTION_MASTER_KEYS: ${ENCRYPTION_MASTER_KEYS:-}
ENCRYPTION_WRITE_VERSION: ${ENCRYPTION_WRITE_VERSION:-v2}
services:
postgres:
image: postgres:18
environment:
POSTGRES_DB: ${POSTGRES_DB:-gitterm}
POSTGRES_USER: ${POSTGRES_USER:-gitterm}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
volumes:
- postgres_data:/var/lib/postgresql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
server:
image: ${GITTERM_IMAGE_PREFIX:-ghcr.io/opeoginni/gitterm}-server:${GITTERM_IMAGE_TAG:-latest}
build:
context: .
dockerfile: apps/server/Dockerfile
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
environment:
<<: *internal-env
PORT: 8080
DATABASE_URL: postgresql://${POSTGRES_USER:-gitterm}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-gitterm}
REDIS_URL: redis://redis:6379
ROUTING_MODE: ${ROUTING_MODE:-path}
BASE_URL: ${PUBLIC_URL:?PUBLIC_URL is required}
BASE_DOMAIN: ${PUBLIC_HOST:?PUBLIC_HOST is required}
CORS_ORIGIN: ${PUBLIC_URL}
BETTER_AUTH_URL: ${PUBLIC_URL}/api
API_URL: ${PUBLIC_URL}/api
WORKSPACE_API_URL: ${PUBLIC_URL}/api/trpc
DEVICE_CODE_VERIFICATION_URI: ${PUBLIC_URL}/device
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?BETTER_AUTH_SECRET is required}
AUTH_ENCRYPT_OAUTH_TOKENS: ${AUTH_ENCRYPT_OAUTH_TOKENS:-false}
WORKSPACE_JWT_SECRET: ${WORKSPACE_JWT_SECRET:?WORKSPACE_JWT_SECRET is required}
ADMIN_EMAIL: ${ADMIN_EMAIL:-}
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-}
GITHUB_APP_CLIENT_ID: ${GITHUB_APP_CLIENT_ID:-}
GITHUB_APP_CLIENT_SECRET: ${GITHUB_APP_CLIENT_SECRET:-}
ENABLE_IDLE_REAPING: ${ENABLE_IDLE_REAPING:-true}
EMAIL_PROVIDER: ${EMAIL_PROVIDER:-auto}
EMAIL_FROM: ${EMAIL_FROM:-GitTerm <no-reply@example.com>}
RESEND_API_KEY: ${RESEND_API_KEY:-}
SMTP_HOST: ${SMTP_HOST:-}
SMTP_PORT: ${SMTP_PORT:-587}
SMTP_USER: ${SMTP_USER:-}
SMTP_PASS: ${SMTP_PASS:-}
SKIP_MIGRATIONS: ${SKIP_MIGRATIONS:-false}
SKIP_SEED: ${SKIP_SEED:-false}
SKIP_ADMIN_SEED: ${SKIP_ADMIN_SEED:-false}
healthcheck:
test:
- CMD
- bun
- -e
- "fetch('http://localhost:8080/').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
interval: 15s
timeout: 5s
retries: 10
start_period: 90s
restart: unless-stopped
listener:
image: ${GITTERM_IMAGE_PREFIX:-ghcr.io/opeoginni/gitterm}-listener:${GITTERM_IMAGE_TAG:-latest}
build:
context: .
dockerfile: apps/listener/Dockerfile
depends_on:
server:
condition: service_healthy
environment:
<<: *internal-env
PORT: 8080
WORKSPACE_JWT_SECRET: ${WORKSPACE_JWT_SECRET:?WORKSPACE_JWT_SECRET is required}
BASE_DOMAIN: ${PUBLIC_HOST}
CORS_ORIGIN: ${PUBLIC_URL}
restart: unless-stopped
worker:
image: ${GITTERM_IMAGE_PREFIX:-ghcr.io/opeoginni/gitterm}-idle-reaper-worker:${GITTERM_IMAGE_TAG:-latest}
build:
context: .
dockerfile: apps/worker/Dockerfile
depends_on:
server:
condition: service_healthy
environment:
<<: *internal-env
ENABLE_IDLE_REAPING: ${ENABLE_IDLE_REAPING:-true}
# Minutes between reap passes. Must be >0 here: the worker loops in-process
# because Compose has no external cron. 0 = run once and exit (Railway Cron mode).
REAP_INTERVAL_MINUTES: ${REAP_INTERVAL_MINUTES:-10}
restart: unless-stopped
web:
image: ${GITTERM_IMAGE_PREFIX:-ghcr.io/opeoginni/gitterm}-web:${GITTERM_IMAGE_TAG:-latest}
build:
context: .
dockerfile: apps/web/Dockerfile
depends_on:
server:
condition: service_healthy
environment:
PORT: 8080
HOSTNAME: 0.0.0.0
NEXT_PUBLIC_BASE_DOMAIN: ${PUBLIC_HOST}
NEXT_PUBLIC_SERVER_URL: ${PUBLIC_URL}/api
NEXT_PUBLIC_AUTH_URL: ${PUBLIC_URL}
NEXT_PUBLIC_LISTENER_URL: ${PUBLIC_URL}/listener
NEXT_PUBLIC_DEPLOYMENT_MODE: self-hosted
NEXT_PUBLIC_ROUTING_MODE: ${ROUTING_MODE:-path}
NEXT_PUBLIC_ENABLE_BILLING: "false"
restart: unless-stopped
proxy:
image: ${GITTERM_IMAGE_PREFIX:-ghcr.io/opeoginni/gitterm}-proxy:${GITTERM_IMAGE_TAG:-latest}
build:
context: .
dockerfile: apps/proxy/Dockerfile
depends_on:
- server
- web
- listener
ports:
- "${GITTERM_PORT:-8888}:80"
environment:
ROUTING_MODE: ${ROUTING_MODE:-path}
BASE_DOMAIN: ${PUBLIC_HOST}
WEB_URL: web:8080
SERVER_URL: server:8080
LISTENER_URL: listener:8080
INTERNAL_API_KEY: ${INTERNAL_API_KEY}
restart: unless-stopped
volumes:
postgres_data:
redis_data: