# Tx OS — production-style compose stack for a single VPS. # # Bring up: docker compose up -d # Migrate DB: docker compose run --rm migrate # Logs: docker compose logs -f api # Tear down: docker compose down # # All secrets (passwords, session secret, MinIO credentials) live in `.env`. # Copy `.env.example` to `.env` and edit before first boot. name: tx-os services: db: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - db_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"] interval: 10s timeout: 5s retries: 10 networks: [internal] minio: image: minio/minio:RELEASE.2025-01-20T14-49-07Z restart: unless-stopped command: server /data --console-address ":9001" environment: MINIO_ROOT_USER: ${S3_ACCESS_KEY_ID} MINIO_ROOT_PASSWORD: ${S3_SECRET_ACCESS_KEY} volumes: - minio_data:/data healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 15s timeout: 5s retries: 5 networks: [internal] # One-shot: create the two buckets on first boot. Idempotent. minio-init: image: minio/mc:RELEASE.2025-01-17T23-25-50Z depends_on: minio: condition: service_healthy entrypoint: > /bin/sh -c " mc alias set local http://minio:9000 $${S3_ACCESS_KEY_ID} $${S3_SECRET_ACCESS_KEY} && mc mb --ignore-existing local/${S3_BUCKET_PRIVATE} && mc mb --ignore-existing local/${S3_BUCKET_PUBLIC} && mc anonymous set download local/${S3_BUCKET_PUBLIC} || true " environment: S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID} S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY} S3_BUCKET_PRIVATE: ${S3_BUCKET_PRIVATE} S3_BUCKET_PUBLIC: ${S3_BUCKET_PUBLIC} networks: [internal] restart: "no" api: build: context: . target: api image: tx-os/api:latest restart: unless-stopped depends_on: db: condition: service_healthy minio: condition: service_healthy environment: NODE_ENV: production PORT: "8080" DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} SESSION_SECRET: ${SESSION_SECRET} ALLOWED_ORIGINS: ${ALLOWED_ORIGINS} LOG_LEVEL: ${LOG_LEVEL:-info} PUBLIC_BASE_URL: ${PUBLIC_BASE_URL} # Storage — S3 driver against MinIO. STORAGE_DRIVER: s3 S3_ENDPOINT: http://minio:9000 S3_REGION: ${S3_REGION:-us-east-1} S3_ACCESS_KEY_ID: ${S3_ACCESS_KEY_ID} S3_SECRET_ACCESS_KEY: ${S3_SECRET_ACCESS_KEY} S3_FORCE_PATH_STYLE: "true" PRIVATE_OBJECT_DIR: /${S3_BUCKET_PRIVATE}/private PUBLIC_OBJECT_SEARCH_PATHS: /${S3_BUCKET_PUBLIC}/public # Email (optional — leave SMTP_HOST blank to disable mail). SMTP_HOST: ${SMTP_HOST:-} SMTP_PORT: ${SMTP_PORT:-} SMTP_USER: ${SMTP_USER:-} SMTP_PASS: ${SMTP_PASS:-} SMTP_SECURE: ${SMTP_SECURE:-} SMTP_FROM: ${SMTP_FROM:-} ports: # API — exposed on host :8080 for direct curl/integration access. In # a public-facing deployment this should be bound to 127.0.0.1 only # (e.g. "127.0.0.1:8080:8080") so only the edge proxy reaches it. - "${API_PORT:-8080}:8080" networks: [internal] healthcheck: test: ["CMD-SHELL", "node -e \"require('http').get('http://localhost:8080/api/healthz', r => process.exit(r.statusCode===200?0:1)).on('error', () => process.exit(1))\""] interval: 15s timeout: 5s retries: 10 web: build: context: . target: web image: tx-os/web:latest restart: unless-stopped depends_on: api: condition: service_healthy ports: # SPA — exposed on host :3000. Front this with an operator-managed # TLS terminator (Caddy / Nginx / Traefik) for production HTTPS. - "${WEB_PORT:-3000}:80" networks: [internal] # Component preview server. Dev-only — used to iterate on isolated # React components in an iframe-friendly preview at /__mockup. Not part # of the production user surface; kept under the `dev` profile so a # normal `docker compose up -d` does NOT start it. Run with: # docker compose --profile dev up -d mockup-sandbox mockup-sandbox: build: context: . target: mockup image: tx-os/mockup:latest profiles: ["dev"] environment: NODE_ENV: development PORT: "8081" BASE_PATH: /__mockup ports: - "${MOCKUP_PORT:-8081}:8081" networks: [internal] # One-shot DB migrate + seed. Run on first boot: # docker compose run --rm migrate migrate: build: context: . target: migrate image: tx-os/migrate:latest profiles: ["tools"] depends_on: db: condition: service_healthy environment: DATABASE_URL: postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB} NODE_ENV: production SEED_ADMIN_PASSWORD: ${SEED_ADMIN_PASSWORD} SEED_USER_PASSWORD: ${SEED_USER_PASSWORD} networks: [internal] volumes: db_data: minio_data: networks: internal: driver: bridge