Files
tanabata/docker-compose.yml
T
H1K0 19bdd3faa9 build(project): add one-shot dedup compose service
Adds a `dedup` task service under the "tools" profile so it's kept out of
`docker compose up` and run on demand:

  docker compose run --rm dedup            # hashes, then rebuild pairs
  docker compose run --rm dedup -pairs     # only rebuild pairs
  docker compose run --rm dedup -hashes    # only backfill hashes

It reuses the app image, .env, volumes and networks, overriding only the
entrypoint to /app/dedup. Unlike `docker exec` on the live server, this runs in
its own container and is self-documented for cron/CI use.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-16 13:20:27 +03:00

171 lines
6.3 KiB
YAML

# =============================================================================
# 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
# 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: