# Ririko AI 2.0 production stack: PostgreSQL, Lavalink, the bot and the dashboard.
#
#   cp .env.production.example .env.production   # fill in the required values
#   docker compose -f docker-compose.production.yml --env-file .env.production up -d
#
# docs/deployment.md explains every service, volume and setting.
name: ririko

x-app-environment: &app-environment
  DATABASE_DIALECT: postgres
  DATABASE_URL: postgres://${POSTGRES_USER:-ririko}:${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-ririko}

# Rotate container logs so they cannot fill the disk: 5 files of 20 MB per service.
x-logging: &logging
  driver: local
  options:
    max-size: 20m
    max-file: '5'

services:
  postgres:
    image: postgres:16-alpine
    restart: unless-stopped
    logging: *logging
    # Keeps its default capabilities: the entrypoint chowns the data directory and drops to the
    # postgres user.
    security_opt:
      - no-new-privileges:true
    environment:
      POSTGRES_USER: ${POSTGRES_USER:-ririko}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD}
      POSTGRES_DB: ${POSTGRES_DB:-ririko}
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U "$$POSTGRES_USER" -d "$$POSTGRES_DB"']
      interval: 10s
      timeout: 5s
      retries: 5

  # One-shot: the image runs as uid 322 but has no plugins directory, so Docker creates the named
  # volume owned by root and Lavalink cannot write its plugins. Same image, so nothing extra is
  # pulled; it needs only CAP_CHOWN and no network.
  lavalink-plugins:
    image: ghcr.io/lavalink-devs/lavalink:4.2.2
    restart: 'no'
    logging: *logging
    user: root
    entrypoint: ['chown', '-R', '322:322', '/opt/Lavalink/plugins']
    network_mode: none
    cap_drop:
      - ALL
    cap_add:
      - CHOWN
    security_opt:
      - no-new-privileges:true
    volumes:
      - lavalink_plugins:/opt/Lavalink/plugins

  lavalink:
    image: ghcr.io/lavalink-devs/lavalink:4.2.2
    restart: unless-stopped
    logging: *logging
    security_opt:
      - no-new-privileges:true
    # application.yml reads these through Spring placeholders.
    environment:
      LAVALINK_PASSWORD: ${LAVALINK_PASSWORD:?set LAVALINK_PASSWORD}
      SPOTIFY_CLIENT_ID: ${SPOTIFY_CLIENT_ID:-}
      SPOTIFY_CLIENT_SECRET: ${SPOTIFY_CLIENT_SECRET:-}
      SPOTIFY_SP_DC: ${SPOTIFY_SP_DC:-}
      _JAVA_OPTIONS: -Xmx${LAVALINK_HEAP:-1G}
    depends_on:
      lavalink-plugins:
        condition: service_completed_successfully
    volumes:
      - ./docker/lavalink/application.yml:/opt/Lavalink/application.yml:ro
      # Plugins download on first start; keep them between restarts.
      - lavalink_plugins:/opt/Lavalink/plugins

  bot:
    image: ririkoai/ririkobot:${RIRIKO_VERSION:-2}
    build:
      context: .
      target: bot-runner
    restart: unless-stopped
    logging: *logging
    # Runs as uid 10001 and needs no Linux capability.
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    env_file: ${RIRIKO_ENV_FILE:-.env.production}
    environment:
      <<: *app-environment
      LAVALINK_HOST: lavalink
      LAVALINK_PORT: '2333'
      LAVALINK_PASSWORD: ${LAVALINK_PASSWORD:?set LAVALINK_PASSWORD}
      # The bot applies pending schema migrations when it starts, so a plain `docker compose pull
      # && up -d` upgrades a self-hosted stack. The host deploy (ririko-deploy) runs every compose
      # call with DB_AUTO_MIGRATE=false instead and migrates as its own step, from the new image,
      # before the stack starts (ADR-015). The dashboard never migrates.
      DB_AUTO_MIGRATE: ${DB_AUTO_MIGRATE:-true}
    depends_on:
      postgres:
        condition: service_healthy
      lavalink:
        condition: service_started
    volumes:
      - ririko_data:/app/data
      - card_images:/app/public/cards
      - boss_images:/app/public/bosses
      - welcomer_backgrounds:/app/storage/welcomer-backgrounds
      # Upgrading from 1.4.0: the migration does not support this Postgres stack yet. Upgrade
      # with the SQLite compose in docs/upgrading-from-1.4.md. Once Postgres targets work, mount
      # the untouched 1.4.0 data folder read-only here; it is migrated once on first start.
      # - ./data:/app/legacy:ro

  web:
    image: ririkoai/ririkobot-dashboard:${RIRIKO_VERSION:-2}
    build:
      context: .
      target: web-runner
    restart: unless-stopped
    logging: *logging
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    env_file: ${RIRIKO_ENV_FILE:-.env.production}
    environment:
      <<: *app-environment
    depends_on:
      postgres:
        condition: service_healthy
    # Loopback by default: put a reverse proxy or tunnel on the host in front of it.
    ports:
      - '${DASHBOARD_BIND:-127.0.0.1}:${DASHBOARD_PORT:-3000}:3000'
    volumes:
      # Card art in data/tcg, rendered cards and uploaded welcome backgrounds are shared with
      # the bot.
      - ririko_data:/app/data
      - card_images:/app/public/cards
      - welcomer_backgrounds:/app/storage/welcomer-backgrounds

# Named volumes start owned by the images' app user (uid 10001). A bind mount in their place
# must be made writable for it first: chown -R 10001:10001 <dir>.
volumes:
  postgres_data:
  lavalink_plugins:
  ririko_data:
  card_images:
  boss_images:
  welcomer_backgrounds:
