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: # Pull the prebuilt image from the Gitea registry (published by # .gitea/workflows/build-images.yml on every merge to main). `build:` is kept # so a local `docker compose build`/`up --build` still works; on the server, # `docker compose pull && up -d` uses the registry image and never builds. # IMAGE_TAG defaults to `latest`; pin a specific build for reproducible # deploys / rollback, e.g. IMAGE_TAG=sha-042a151 (see .env / .env.example). image: gitea.whitlocktech.com/uom/website-app:${IMAGE_TAG:-latest} build: . 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: pull the prebuilt bot image; IMAGE_TAG pins the build. image: gitea.whitlocktech.com/uom/website-bot:${IMAGE_TAG:-latest} build: context: . dockerfile: bot/Dockerfile 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: