docker-compose.yml
Rama main · réplica de lectura del repositorio original.
# ─────────────────────────────────────────────────────────────
# Self-hosting stack: postgres + rustfs + api + worker + bot + web.
# Coolify uses docker-compose.coolify.yml; local dev uses docker-compose.dev.yml
# (backing services only).
#
# cp .env.example .env # fill in the required values below
# docker compose up -d
#
# Required in .env (the stack refuses to start without them):
# POSTGRES_PASSWORD, BETTER_AUTH_SECRET, APP_ENCRYPTION_KEY,
# S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, API_URL, APP_URL
# Generate each secret with: openssl rand -base64 32
#
# The four services run from images published on each release; update with
# docker compose pull && docker compose up -d
# VERSION in .env pins a release (defaults to the latest one). To run this
# checkout instead of the published images, build them:
# docker compose up -d --build
# ─────────────────────────────────────────────────────────────
name: itsaplan
services:
postgres:
image: postgres:17-alpine
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-itsaplan}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
POSTGRES_DB: ${POSTGRES_DB:-itsaplan}
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U ${POSTGRES_USER:-itsaplan} -d ${POSTGRES_DB:-itsaplan}']
interval: 5s
timeout: 5s
retries: 10
# S3-compatible object store for issue attachments (RustFS).
# Both the S3 API (:9000) and the console (:9001) stay inside the compose
# network: the api streams attachment bytes through itself, so no port is
# published. Administer through an S3 client from inside the network or an SSH tunnel.
#
# The service and volume keep the names of the MinIO stack this replaced: a renamed
# service would leave the old MinIO container running on the same volume, and a
# renamed volume would start empty. RustFS reads the files MinIO wrote in place;
# MinIO wrote them as root, so the container runs as root to keep them writable.
minio:
image: rustfs/rustfs:1.0.0
restart: unless-stopped
user: '0:0'
environment:
RUSTFS_ACCESS_KEY: ${S3_ACCESS_KEY_ID:?set S3_ACCESS_KEY_ID in .env}
RUSTFS_SECRET_KEY: ${S3_SECRET_ACCESS_KEY:?set S3_SECRET_ACCESS_KEY in .env}
volumes:
- minio-data:/data
healthcheck:
test: ['CMD', 'curl', '-fsS', '-o', '/dev/null', 'http://127.0.0.1:9000/health']
interval: 15s
timeout: 5s
retries: 5
start_period: 20s
# One-shot: creates the attachments bucket, then exits. A PUT on a bucket that
# already exists succeeds, so it runs on every start.
minio-init:
image: rustfs/rustfs:1.0.0
restart: 'no'
depends_on:
minio:
condition: service_healthy
environment:
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
S3_BUCKET: ${S3_BUCKET:-planner-attachments}
entrypoint: >
/bin/sh -c "
curl -fsS -X PUT --aws-sigv4 aws:amz:us-east-1:s3 --user \"$$S3_ACCESS_KEY_ID:$$S3_SECRET_ACCESS_KEY\" http://minio:9000/\"$$S3_BUCKET\" &&
echo 'bucket ready: '\"$$S3_BUCKET\"
"
# One-shot: dumps the database and applies the migrations, then exits. api, worker
# and bot start only after it succeeds, so none of them runs against an old schema.
# restart 'no' keeps a finished run from being started again.
migrate:
image: ghcr.io/croffasia/itsaplan-api:${VERSION:-latest}
build:
context: .
dockerfile: apps/api/Dockerfile
restart: 'no'
command: ['bun', 'run', 'packages/db/src/migrate.ts']
depends_on:
postgres:
condition: service_healthy
environment:
DATABASE_URL: postgres://${POSTGRES_USER:-itsaplan}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-itsaplan}
# How long the dumps are kept, and the switch that upgrades without one.
# BACKUP_DIR is not among them: the volume below pins it.
BACKUP_RETENTION_DAYS: ${BACKUP_RETENTION_DAYS:-}
SKIP_PRE_MIGRATION_BACKUP: ${SKIP_PRE_MIGRATION_BACKUP:-}
# Holds the dump taken before a release's migrations, kept for
# BACKUP_RETENTION_DAYS (30) so a downgrade has something to restore.
volumes:
- db-backups:/backups
api:
image: ghcr.io/croffasia/itsaplan-api:${VERSION:-latest}
build:
context: .
dockerfile: apps/api/Dockerfile
restart: unless-stopped
# The image's own CMD migrates first; here the migrate service has done that.
command: ['bun', 'run', 'apps/api/src/index.ts']
depends_on:
postgres:
condition: service_healthy
migrate:
condition: service_completed_successfully
minio-init:
condition: service_completed_successfully
environment:
NODE_ENV: production
API_PORT: 3000
DATABASE_URL: postgres://${POSTGRES_USER:-itsaplan}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-itsaplan}
BETTER_AUTH_SECRET: ${BETTER_AUTH_SECRET:?set BETTER_AUTH_SECRET in .env}
# Encrypts stored provider credentials at rest (AES-256-GCM). Changing it
# makes existing stored credentials undecryptable.
APP_ENCRYPTION_KEY: ${APP_ENCRYPTION_KEY:?set APP_ENCRYPTION_KEY in .env}
# Public origins, each entered once. API_URL is the api origin (auth handler,
# email links); APP_URL is the web origin (CORS, passkey, email links).
API_URL: ${API_URL:?public origin of the api, e.g. https://api.example.com}
APP_URL: ${APP_URL:?public origin of the web app, e.g. https://app.example.com}
# Set COOKIE_DOMAIN only to override the parent domain derived from
# APP_URL (multi-label TLDs, deep subdomains).
COOKIE_DOMAIN: ${COOKIE_DOMAIN:-}
# Where the client IP for the rate limit is read; empty reads X-Forwarded-For.
CLIENT_IP_HEADER: ${CLIENT_IP_HEADER:-}
TRUSTED_PROXIES: ${TRUSTED_PROXIES:-}
# Public origin for incoming Git webhooks when API_URL is private; empty uses API_URL.
GIT_WEBHOOK_BASE_URL: ${GIT_WEBHOOK_BASE_URL:-}
S3_ENDPOINT: http://minio:9000
S3_BUCKET: ${S3_BUCKET:-planner-attachments}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
S3_REGION: ${S3_REGION:-us-east-1}
S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-}
# Hosts trusted even though they resolve privately. Also governs webhook URL
# validation and attachment import.
SSRF_ALLOWED_HOSTS: ${SSRF_ALLOWED_HOSTS:-}
healthcheck:
test: ['CMD', 'wget', '-q', '-O', '/dev/null', 'http://127.0.0.1:3000/']
interval: 15s
timeout: 5s
retries: 5
start_period: 30s
ports:
- '${API_PORT:-3000}:3000'
# Webhook delivery, notification delivery, agent schedules, and agent runs.
# Shares the database with the api. No public ports.
worker:
image: ghcr.io/croffasia/itsaplan-worker:${VERSION:-latest}
build:
context: .
dockerfile: apps/worker/Dockerfile
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
migrate:
condition: service_completed_successfully
environment:
NODE_ENV: production
DATABASE_URL: postgres://${POSTGRES_USER:-itsaplan}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-itsaplan}
# Notification delivery sends email and Telegram messages itself, with the
# provider credentials stored in the database — encrypted with this key, the
# same value the api uses.
APP_ENCRYPTION_KEY: ${APP_ENCRYPTION_KEY:?set APP_ENCRYPTION_KEY in .env}
# Anonymous telemetry (TELEMETRY.md). Either variable, set to anything but 0,
# turns it off.
TELEMETRY_DISABLED: ${TELEMETRY_DISABLED:-}
DO_NOT_TRACK: ${DO_NOT_TRACK:-}
TELEMETRY_DEBUG: ${TELEMETRY_DEBUG:-}
# The same list the api reads. This process delivers webhooks to those hosts.
SSRF_ALLOWED_HOSTS: ${SSRF_ALLOWED_HOSTS:-}
# A downloaded Plane import attachment is stored here, the same bucket the
# api's own uploads use.
S3_ENDPOINT: http://minio:9000
S3_BUCKET: ${S3_BUCKET:-planner-attachments}
S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID}
S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY}
S3_REGION: ${S3_REGION:-us-east-1}
S3_FORCE_PATH_STYLE: ${S3_FORCE_PATH_STYLE:-}
# Optional tuning lives in apps/worker/src/config.ts.
# Telegram bot: long polling for the instance bot, completing the account links
# users start from their profile. The bot token is stored in the database and
# edited in god mode, so it is not an environment variable here.
#
# Must stay at one replica: Telegram hands each getUpdates call to a single
# caller, so a second instance would take updates away from the first.
bot:
image: ghcr.io/croffasia/itsaplan-bot:${VERSION:-latest}
build:
context: .
dockerfile: apps/bot/Dockerfile
restart: unless-stopped
depends_on:
postgres:
condition: service_healthy
migrate:
condition: service_completed_successfully
environment:
NODE_ENV: production
DATABASE_URL: postgres://${POSTGRES_USER:-itsaplan}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB:-itsaplan}
APP_ENCRYPTION_KEY: ${APP_ENCRYPTION_KEY:?set APP_ENCRYPTION_KEY in .env}
# The origins are read at startup and handed to the browser by the server render,
# so the image carries none of them and a changed API_URL only needs a restart.
web:
image: ghcr.io/croffasia/itsaplan-web:${VERSION:-latest}
build:
context: .
dockerfile: apps/web/Dockerfile
restart: unless-stopped
depends_on:
- api
environment:
NODE_ENV: production
PORT: 3001
HOSTNAME: 0.0.0.0
# The api origin the browser uses.
API_URL: ${API_URL:?public origin of the api, e.g. https://api.example.com}
# Where web itself reaches the api: it streams avatars and attachments from
# there (/media) so they are served from the web origin.
SERVICE_URL_API: http://api:3000
# Legal document URLs shown on the logged-out screens; empty hides the notice.
PRIVACY_URL: ${PRIVACY_URL:-}
TERMS_URL: ${TERMS_URL:-}
# /login renders without a session, and without reaching the api.
healthcheck:
test: ['CMD', 'wget', '-q', '-O', '/dev/null', 'http://127.0.0.1:3001/login']
interval: 15s
timeout: 5s
retries: 5
start_period: 20s
ports:
- '${WEB_PORT:-3001}:3001'
volumes:
postgres-data:
minio-data:
db-backups: