# =============================================================================
# Tanabata File Manager — Docker Compose
#
# Quick start:
#   cp .env.example .env        # then edit the secrets
#   docker compose up -d --build
#
# Database — two supported modes, selected in .env:
#
#   1. Bundled Postgres container (default).
#        COMPOSE_PROFILES=with-db
#        DATABASE_URL=postgres://tanabata:password@db:5432/tanabata?sslmode=disable
#
#   2. Postgres already running on the host.
#        COMPOSE_PROFILES=          # empty → the db container is not started
#        DATABASE_URL=postgres://tanabata:password@host.docker.internal:5432/tanabata?sslmode=disable
#
# Requires Docker Compose v2.20+ (for depends_on.required).
# =============================================================================

services:
  app:
    build:
      context: .
      dockerfile: Dockerfile
    container_name: tfm
    restart: unless-stopped

    # Give the app time to drain in-flight requests on stop before SIGKILL. Reads
    # the same SHUTDOWN_TIMEOUT the app uses for its graceful-shutdown deadline
    # (via env_file below), so the two never drift. Interpolated from .env at
    # `docker compose` time; defaults to 15s if unset.
    stop_grace_period: ${SHUTDOWN_TIMEOUT:-15s}

    # All application config (secrets, DATABASE_URL, tunables) comes from .env.
    env_file: .env

    # Pin STATIC_DIR to the path baked into the image. .env intentionally leaves
    # it unset; pinning here guarantees in-container SPA serving can't be
    # disabled by an empty value leaking in through env_file.
    environment:
      STATIC_DIR: /app/static

    # Published on loopback only: a reverse proxy on the host (e.g. nginx) fronts
    # the app and proxies to 127.0.0.1:${APP_PORT}. Binding to 127.0.0.1 keeps the
    # app off the LAN/WAN — a plain "PORT:42776" would publish on 0.0.0.0 and, since
    # Docker's DNAT rules sit ahead of the host firewall, bypass ufw/firewalld. The
    # container always listens on 42776 (Dockerfile default); APP_PORT only changes
    # the host-published port. Drop the 127.0.0.1 prefix if exposing it directly.
    ports:
      - "127.0.0.1:${APP_PORT:-42776}:42776"

    # Two-tier networking. `web` is the app's public-facing bridge (reached via the
    # published loopback port above; it also provides egress, e.g. to a host
    # Postgres via host.docker.internal). `backend` is the private tier the app
    # uses to reach the bundled DB. The DB sits only on `backend`, so nothing on
    # the host-facing side can reach it.
    networks:
      - web
      - backend

    # Wait for the bundled DB when the with-db profile is active. When using a
    # host Postgres the db service is disabled, and required:false keeps this
    # dependency from erroring or auto-starting it.
    depends_on:
      db:
        condition: service_healthy
        required: false

    # Lets DATABASE_URL reach a Postgres on the host via host.docker.internal
    # (needed on Linux; harmless elsewhere).
    extra_hosts:
      - "host.docker.internal:host-gateway"

    # Run as this uid:gid. Relevant when the mounts below are bind-mounted to
    # host folders: set PUID/PGID (in .env) to the owner of those folders so the
    # container can write to them. Defaults to the image's tanabata user
    # (42776), which owns the named volumes.
    user: "${PUID:-42776}:${PGID:-42776}"

    # Storage for originals, the thumbnail cache, and the import drop folder.
    # Each source defaults to a named volume but can be pointed at a specific
    # host folder via FILES_DIR / THUMBS_DIR / IMPORT_DIR in .env (a path turns
    # the mount into a host bind mount; a bare name stays a named volume).
    volumes:
      - "${FILES_DIR:-app_files}:/data/files"
      - "${THUMBS_DIR:-app_thumbs}:/data/thumbs"
      - "${IMPORT_DIR:-app_import}:/data/import"

  db:
    image: postgres:14-alpine
    restart: unless-stopped

    # Only started when COMPOSE_PROFILES includes "with-db". Disable it to point
    # the app at a Postgres running on the host instead.
    profiles: ["with-db"]

    # Private back-end tier only — never on `web`, never published.
    networks:
      - backend

    environment:
      POSTGRES_DB: ${POSTGRES_DB:-tanabata}
      POSTGRES_USER: ${POSTGRES_USER:-tanabata}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-password}

    # Defaults to a named volume; set DB_DIR in .env to a host folder to bind
    # mount it instead. Postgres fixes the folder's ownership itself, so DB_DIR
    # needs no PUID/PGID.
    volumes:
      - "${DB_DIR:-db_data}:/var/lib/postgresql/data"

    # Uncomment to reach the DB from the host (e.g. with psql) for debugging.
    # ports:
    #   - "5432:5432"

    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-tanabata} -d ${POSTGRES_DB:-tanabata}"]
      interval: 5s
      timeout: 5s
      retries: 10

  # One-shot maintenance task for duplicate detection: computes missing
  # perceptual hashes (images + video) and rebuilds the duplicate-pairs table.
  # It is NOT a daemon — the "tools" profile keeps it out of `docker compose up`;
  # run it on demand, and it exits when done:
  #
  #   docker compose run --rm dedup            # hashes, then rebuild pairs
  #   docker compose run --rm dedup -pairs     # only rebuild pairs (after uploads)
  #   docker compose run --rm dedup -hashes    # only backfill hashes
  #
  # Reuses the app image, .env, volumes and networks; only the entrypoint differs
  # (/app/dedup instead of the server). Connects to the same DB the app uses, so
  # the app's DB (bundled or host) must be reachable when it runs.
  dedup:
    build:
      context: .
      dockerfile: Dockerfile
    profiles: ["tools"]
    env_file: .env
    networks:
      - web
      - backend
    depends_on:
      db:
        condition: service_healthy
        required: false
    extra_hosts:
      - "host.docker.internal:host-gateway"
    user: "${PUID:-42776}:${PGID:-42776}"
    volumes:
      - "${FILES_DIR:-app_files}:/data/files"
      - "${THUMBS_DIR:-app_thumbs}:/data/thumbs"
    entrypoint: ["/app/dedup"]
    restart: "no"

networks:
  # Public-facing bridge for this app. The explicit bridge name (instead of
  # Docker's random br-<hash>) makes it identifiable on the host for tcpdump and
  # firewall rules.
  web:
    driver_opts:
      com.docker.network.bridge.name: dk-tanabata
  # Private back-end tier (app ↔ DB). internal:true drops the gateway so the DB
  # has no route off-host. Note: Linux caps interface names at 15 chars, and
  # dk-tanabata-bnd is exactly 15 — a longer app name would need a shorter suffix.
  backend:
    internal: true
    driver_opts:
      com.docker.network.bridge.name: dk-tanabata-bnd

volumes:
  app_files:
  app_thumbs:
  app_import:
  db_data:
