# GamePanel — single-host deployment. # # ./scripts/install.sh generates .env with fresh secrets # docker compose up -d builds and starts everything # # Serves plain HTTP on ${WEB_PORT} by design. Put your own reverse proxy # (Caddy, nginx, Traefik, Cloudflare Tunnel…) in front of it for TLS and a # domain — see INSTALLATION.md. services: # --- PostgreSQL --- postgres: image: postgres:16-alpine container_name: gamepanel-postgres restart: unless-stopped environment: POSTGRES_USER: ${DB_USER:-gamepanel} POSTGRES_PASSWORD: ${DB_PASSWORD:-gamepanel} POSTGRES_DB: ${DB_NAME:-gamepanel} volumes: - postgres_data:/var/lib/postgresql/data # Not published by default — only the API needs it. Set DB_PORT to expose it. expose: - "5432" healthcheck: test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-gamepanel}"] interval: 10s timeout: 5s retries: 5 # --- Redis (rate limiting, session cache) --- redis: image: redis:7-alpine container_name: gamepanel-redis restart: unless-stopped command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD:-gamepanel} volumes: - redis_data:/data expose: - "6379" healthcheck: test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD:-gamepanel}", "ping"] interval: 10s timeout: 5s retries: 5 # --- Schema migration + seed (runs to completion, then exits) --- migrate: build: context: . dockerfile: apps/api/Dockerfile target: migrate container_name: gamepanel-migrate restart: "no" depends_on: postgres: condition: service_healthy environment: DATABASE_URL: postgresql://${DB_USER:-gamepanel}:${DB_PASSWORD:-gamepanel}@postgres:5432/${DB_NAME:-gamepanel} # --- API --- api: build: context: . dockerfile: apps/api/Dockerfile container_name: gamepanel-api restart: unless-stopped depends_on: postgres: condition: service_healthy redis: condition: service_healthy migrate: condition: service_completed_successfully environment: NODE_ENV: production DATABASE_URL: postgresql://${DB_USER:-gamepanel}:${DB_PASSWORD:-gamepanel}@postgres:5432/${DB_NAME:-gamepanel} REDIS_URL: redis://:${REDIS_PASSWORD:-gamepanel}@redis:6379 PORT: 3000 HOST: 0.0.0.0 JWT_SECRET: ${JWT_SECRET:?set JWT_SECRET in .env — run ./scripts/install.sh} JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:?set JWT_REFRESH_SECRET in .env — run ./scripts/install.sh} CORS_ORIGIN: ${CORS_ORIGIN:-http://localhost} RATE_LIMIT_MAX: ${RATE_LIMIT_MAX:-100} RATE_LIMIT_WINDOW_MS: ${RATE_LIMIT_WINDOW_MS:-60000} expose: - "3000" # --- Web (nginx + SPA, also reverse-proxies /api and /socket.io) --- web: build: context: . dockerfile: apps/web/Dockerfile args: VITE_API_URL: /api container_name: gamepanel-web restart: unless-stopped depends_on: - api ports: - "${WEB_PORT:-80}:80" # --- Daemon (runs on game server nodes) --- daemon: build: context: . dockerfile: apps/daemon/Dockerfile container_name: gamepanel-daemon restart: unless-stopped depends_on: - api environment: DAEMON_CONFIG: /etc/gamepanel/config.yml # Game containers are created through the host's Docker socket, so their # bind mounts are resolved by the *host*, not by this container. Without # this the daemon and the game would end up looking at two different # directories and every file written from the panel would vanish. DAEMON_HOST_DATA_PATH: ${DAEMON_DATA_PATH:-/var/lib/gamepanel/servers} volumes: - /var/run/docker.sock:/var/run/docker.sock - ${DAEMON_DATA_PATH:-/var/lib/gamepanel/servers}:/var/lib/gamepanel/servers - ${DAEMON_BACKUP_PATH:-/var/lib/gamepanel/backups}:/var/lib/gamepanel/backups - ./daemon-config.yml:/etc/gamepanel/config.yml:ro ports: - "${DAEMON_GRPC_PORT:-50051}:50051" volumes: postgres_data: redis_data: