Files
website/docker-compose.yml
Claude 6d4cd91bcc deploy: split build into docker-compose.dev.yml overlay
Make the base docker-compose.yml strictly production-shaped — image: only, no
build: — so a production host can only ever pull, never accidentally build
(compose gives build precedence for `up --build`/`build`, which mixed the two
modes). Local builds move to an explicit, non-auto-loaded overlay.

  Production:   docker compose pull && docker compose up -d
  Development:  docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build

Verified with `docker compose config`: base renders image-only (no build) for
both services; the dev overlay adds build back (app -> Dockerfile,
bot -> bot/Dockerfile). README quick-start updated to the two-file flow.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0114TpmrNW4wNXsHq5CR72jQ
2026-07-11 18:42:05 -05:00

88 lines
3.4 KiB
YAML

services:
db:
image: mariadb:11
restart: unless-stopped
environment:
MARIADB_DATABASE: ${DB_NAME}
MARIADB_USER: ${DB_USER}
MARIADB_PASSWORD: ${DB_PASSWORD}
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
volumes:
- dbdata:/var/lib/mysql
- ./server/db/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql:ro
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 10s
timeout: 5s
retries: 10
# No host port published by default — only the app needs the DB, over the
# private compose network. Uncomment to inspect from the host:
# ports:
# - "3306:3306"
app:
# Prebuilt image from the Gitea registry (published by
# .gitea/workflows/build-images.yml on every merge to main). This file is
# production-shaped — image only, NO build: — so a production host can only
# ever pull, never accidentally build. IMAGE_TAG defaults to `latest`; pin a
# specific build for a reproducible deploy / rollback, e.g.
# IMAGE_TAG=sha-042a151 (see .env / .env.example). To build locally instead,
# overlay docker-compose.dev.yml (see README).
image: gitea.whitlocktech.com/uom/website-app:${IMAGE_TAG:-latest}
restart: unless-stopped
env_file: .env
environment:
DB_HOST: db
UPLOAD_DIR: /app/uploads
LOG_DIR: /app/logs
depends_on:
db:
condition: service_healthy
volumes:
- uploads:/app/uploads
# Bind-mount logs to the host so app.log is directly readable at ./logs/
- ./logs:/app/logs
# Only the PUBLIC API port (3000) is published. The internal server<->bot
# port (INTERNAL_PORT, default 3001) is deliberately NOT listed here, so it
# stays reachable only over the private compose network — Pangolin/the public
# reverse proxy can never forward to it. See issue #33.
# Binds 0.0.0.0 (no 127.0.0.1 prefix) so Pangolin can reach the container.
ports:
- "3000:3000"
bot:
# Same as app: prebuilt bot image, pulled in production. Build locally via
# docker-compose.dev.yml.
image: gitea.whitlocktech.com/uom/website-bot:${IMAGE_TAG:-latest}
restart: unless-stopped
env_file: .env
environment:
DB_HOST: db
# Pin the bot's own listen port. Both services share env_file: .env, so
# without this the site's PORT=3000 leaks in and the bot binds 3000 instead
# of 4100 — then the server's BOT_INTERNAL_URL (http://bot:4100) can't reach
# it ("failed to fetch" in the admin panel). Must match that URL's port.
PORT: 4100
# Likewise override the log filename so the bot doesn't inherit the site's
# LOG_FILE and write into app.log — keep the bot's log distinct.
LOG_FILE: bot.log
# Internal config fetch goes to the app's UNPUBLISHED internal port (3001),
# not the public 3000. Keep the port in sync with the app's INTERNAL_PORT.
SITE_INTERNAL_URL: http://app:3001/internal/bot-config
SITE_PUBLIC_URL: http://app:3000/api/v1/public
LOG_DIR: /app/bot/logs
depends_on:
db:
condition: service_healthy
app:
condition: service_started
volumes:
- ./bot/logs:/app/bot/logs
# No published port — the bot's internal API (/internal/*) is reached only
# by `app` over the private compose network, and must NEVER be exposed
# through Pangolin/the public reverse proxy.
volumes:
dbdata:
uploads: