feat(project): add Docker Compose with flexible storage and DB modes
Bundle the app + Postgres into a compose stack on top of the existing image.
- app: builds the image, publishes ${APP_PORT:-42776}, reads .env, pins
STATIC_DIR so SPA serving can't be disabled by an empty value
- db: postgres:14-alpine under the "with-db" profile; toggle it off via
COMPOSE_PROFILES to point the app at a Postgres on the host instead
(host.docker.internal), with depends_on required:false so it stays optional
Storage and the DB data dir each default to a named volume but can be bind
mounted to a host folder via FILES_DIR / THUMBS_DIR / IMPORT_DIR / DB_DIR.
Add PUID/PGID (via user:) so bind-mounted folders are writable by the
non-root container.
Run the container as a dedicated non-root user "tanabata" with uid/gid 42776,
reusing the project's signature number (also the default port). Document every
variable in .env.example.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,10 @@
|
|||||||
.gitignore
|
.gitignore
|
||||||
**/.DS_Store
|
**/.DS_Store
|
||||||
|
|
||||||
|
# Compose file — used to build/run, not needed inside the image context
|
||||||
|
docker-compose.yml
|
||||||
|
docker-compose.*.yml
|
||||||
|
|
||||||
# Secrets and local env
|
# Secrets and local env
|
||||||
.env
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|||||||
+62
-9
@@ -1,8 +1,46 @@
|
|||||||
# =============================================================================
|
# =============================================================================
|
||||||
# Tanabata File Manager — environment variables
|
# Tanabata File Manager — environment variables
|
||||||
# Copy to .env and fill in the values.
|
#
|
||||||
|
# Copy to .env and fill in the secrets:
|
||||||
|
# cp .env.example .env
|
||||||
|
# docker compose up -d --build
|
||||||
# =============================================================================
|
# =============================================================================
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Docker Compose (read by the compose CLI, ignored by the app)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Profiles to enable. "with-db" runs the bundled Postgres container. Leave
|
||||||
|
# EMPTY to skip it and use a Postgres running on the host instead — then point
|
||||||
|
# DATABASE_URL at host.docker.internal (see the Database section below).
|
||||||
|
COMPOSE_PROFILES=with-db
|
||||||
|
|
||||||
|
# Host port the app is published on. The container always listens on 42776.
|
||||||
|
APP_PORT=42776
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Volume mounts (Docker Compose; ignored by the app)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# By default the app's data and the database live in named Docker volumes
|
||||||
|
# (app_files, app_thumbs, app_import, db_data). To keep them in specific folders
|
||||||
|
# on the host instead, point any of these at a host path — absolute, or relative
|
||||||
|
# to this file (e.g. ./data/files). Unset = named volume.
|
||||||
|
# FILES_DIR=/var/lib/tanabata/files
|
||||||
|
# THUMBS_DIR=/var/lib/tanabata/thumbs
|
||||||
|
# IMPORT_DIR=/var/lib/tanabata/import
|
||||||
|
# DB_DIR=/var/lib/tanabata/db
|
||||||
|
|
||||||
|
# When bind-mounting the app folders above, the container must be able to write
|
||||||
|
# to them. Set PUID/PGID to the owner of those folders and create them with
|
||||||
|
# matching ownership first, e.g.:
|
||||||
|
# sudo mkdir -p /var/lib/tanabata/{files,thumbs,import}
|
||||||
|
# sudo chown -R 1000:1000 /var/lib/tanabata
|
||||||
|
# PUID=1000
|
||||||
|
# PGID=1000
|
||||||
|
# Defaults match the image's tanabata user (42776), which owns the named volumes. The
|
||||||
|
# DB folder is handled by Postgres itself and needs no PUID/PGID.
|
||||||
|
# PUID=42776
|
||||||
|
# PGID=42776
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Server
|
# Server
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -21,10 +59,25 @@ ADMIN_PASSWORD=change-me-before-first-run
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Database
|
# Database
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
DATABASE_URL=postgres://tanabata:password@localhost:5432/tanabata?sslmode=disable
|
# Credentials for the bundled Postgres container (the "with-db" profile).
|
||||||
|
# Keep these in sync with DATABASE_URL below.
|
||||||
|
POSTGRES_DB=tanabata
|
||||||
|
POSTGRES_USER=tanabata
|
||||||
|
POSTGRES_PASSWORD=password
|
||||||
|
|
||||||
|
# Connection string the app uses. Pick ONE to match your database mode:
|
||||||
|
#
|
||||||
|
# • Bundled container DB (COMPOSE_PROFILES=with-db) — host is the "db" service:
|
||||||
|
DATABASE_URL=postgres://tanabata:password@db:5432/tanabata?sslmode=disable
|
||||||
|
#
|
||||||
|
# • Postgres on the host (COMPOSE_PROFILES empty):
|
||||||
|
# DATABASE_URL=postgres://tanabata:password@host.docker.internal:5432/tanabata?sslmode=disable
|
||||||
|
#
|
||||||
|
# • Bare-metal `go run` (no Docker):
|
||||||
|
# DATABASE_URL=postgres://tanabata:password@localhost:5432/tanabata?sslmode=disable
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Storage
|
# Storage (paths inside the container; backed by named volumes in compose)
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
FILES_PATH=/data/files
|
FILES_PATH=/data/files
|
||||||
THUMBS_CACHE_PATH=/data/thumbs
|
THUMBS_CACHE_PATH=/data/thumbs
|
||||||
@@ -48,9 +101,9 @@ IMPORT_PATH=/data/import
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Static SPA
|
# Static SPA
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Directory of the built frontend (index.html, _app/, fonts, service worker).
|
# Leave UNSET here. The Docker image already serves the built SPA from
|
||||||
# When set, the server serves the SPA and the API on the same port, with a
|
# /app/static and compose pins STATIC_DIR for the container — an empty value in
|
||||||
# fallback to index.html for client-side routes. Leave empty in local
|
# .env would be injected into the container and disable SPA serving. Set this
|
||||||
# development — the Vite dev server serves the UI separately. The Docker image
|
# only for a bare-metal deploy where the Go server serves a built SPA; leave it
|
||||||
# sets this to /app/static.
|
# unset in local dev, where the Vite dev server serves the UI.
|
||||||
STATIC_DIR=
|
# STATIC_DIR=/path/to/frontend/build
|
||||||
|
|||||||
+6
-6
@@ -58,18 +58,18 @@ FROM alpine:3.21 AS runtime
|
|||||||
RUN apk add --no-cache ffmpeg ca-certificates tzdata
|
RUN apk add --no-cache ffmpeg ca-certificates tzdata
|
||||||
|
|
||||||
# Run as an unprivileged user.
|
# Run as an unprivileged user.
|
||||||
RUN addgroup -S app && adduser -S -G app -u 10001 app
|
RUN addgroup -S -g 42776 tanabata && adduser -S -G tanabata -u 42776 tanabata
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# The built SPA, served by the Go binary (matches STATIC_DIR below).
|
# The built SPA, served by the Go binary (matches STATIC_DIR below).
|
||||||
COPY --from=frontend --chown=app:app /src/frontend/build /app/static
|
COPY --from=frontend --chown=tanabata:tanabata /src/frontend/build /app/static
|
||||||
# The server binary.
|
# The server binary.
|
||||||
COPY --from=backend --chown=app:app /out/server /app/server
|
COPY --from=backend --chown=tanabata:tanabata /out/server /app/server
|
||||||
|
|
||||||
# Data directories (overridable via FILES_PATH/THUMBS_CACHE_PATH/IMPORT_PATH).
|
# Data directories (overridable via FILES_PATH/THUMBS_CACHE_PATH/IMPORT_PATH).
|
||||||
# Created and owned by the app user so a fresh named volume inherits write access.
|
# Created and owned by the tanabata user so a fresh named volume inherits write access.
|
||||||
RUN mkdir -p /data/files /data/thumbs /data/import && chown -R app:app /data
|
RUN mkdir -p /data/files /data/thumbs /data/import && chown -R tanabata:tanabata /data
|
||||||
|
|
||||||
# Non-secret defaults mirroring .env.example. Secrets (JWT_SECRET, ADMIN_PASSWORD,
|
# Non-secret defaults mirroring .env.example. Secrets (JWT_SECRET, ADMIN_PASSWORD,
|
||||||
# DATABASE_URL) are intentionally NOT baked in — pass them at `docker run`.
|
# DATABASE_URL) are intentionally NOT baked in — pass them at `docker run`.
|
||||||
@@ -81,7 +81,7 @@ ENV LISTEN_ADDR=:42776 \
|
|||||||
|
|
||||||
EXPOSE 42776
|
EXPOSE 42776
|
||||||
VOLUME ["/data"]
|
VOLUME ["/data"]
|
||||||
USER app
|
USER tanabata
|
||||||
|
|
||||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
|
||||||
CMD wget -qO- http://127.0.0.1:42776/health >/dev/null 2>&1 || exit 1
|
CMD wget -qO- http://127.0.0.1:42776/health >/dev/null 2>&1 || exit 1
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
# =============================================================================
|
||||||
|
# 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
|
||||||
|
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
|
||||||
|
|
||||||
|
# The container always listens on 42776 (Dockerfile default); APP_PORT only
|
||||||
|
# changes the host-published port.
|
||||||
|
ports:
|
||||||
|
- "${APP_PORT:-42776}:42776"
|
||||||
|
|
||||||
|
# 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"]
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
app_files:
|
||||||
|
app_thumbs:
|
||||||
|
app_import:
|
||||||
|
db_data:
|
||||||
Reference in New Issue
Block a user