Files
job-bot/docker-compose.yml

278 lines
10 KiB
YAML

name: job-bot
# ==============================================================================
# Prepackaged, Zero-Build Docker Compose Stack
# All containers are prebuilt by GitHub Actions CI to ghcr.io/heretek-ai/job-bot
# Remote usage:
# 1. cp .env.example .env && nano .env
# 2. docker compose pull && docker compose up -d
# ==============================================================================
services:
# ============================================================================
# 1. PostgreSQL 16 (Prepackaged with ATS Password Vault & IMAP Sync schema)
# ============================================================================
postgres:
image: ghcr.io/${GHCR_REPO:-heretek-ai/job-bot}/postgres:${IMAGE_TAG:-latest}
restart: unless-stopped
environment:
POSTGRES_USER: ${POSTGRES_USER:-jobpilot}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-jobpilot_secret_password}
POSTGRES_DB: ${POSTGRES_DB:-jobpilot}
volumes:
- postgres-data:/var/lib/postgresql/data
networks:
- app-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-jobpilot} -d ${POSTGRES_DB:-jobpilot}"]
interval: 10s
timeout: 5s
retries: 5
start_period: 10s
# ============================================================================
# 2. Automatic Prisma Database Schema Migration (JobPilot API)
# ============================================================================
db-migrate:
image: ghcr.io/${GHCR_REPO:-heretek-ai/job-bot}/api:${IMAGE_TAG:-latest}
env_file: .env
environment:
DATABASE_URL: ${DATABASE_URL:-postgresql://${POSTGRES_USER:-jobpilot}:${POSTGRES_PASSWORD:-jobpilot_secret_password}@postgres:5432/${POSTGRES_DB:-jobpilot}}
networks:
- app-net
depends_on:
postgres:
condition: service_healthy
command:
- "sh"
- "-c"
- |
HAS_MIGRATIONS=$$(psql "$$DATABASE_URL" -tAc "SELECT to_regclass('public._prisma_migrations');" 2>/dev/null | tr -d ' \r\n' || true)
if [ -z "$$HAS_MIGRATIONS" ]; then
psql "$$DATABASE_URL" -v ON_ERROR_STOP=1 -c "DROP TABLE IF EXISTS public.ats_credentials, public.imap_processed_emails CASCADE;"
fi
exec bun run db:migrate:apply
restart: "no"
# ============================================================================
# 3. Vault Schema Initialization (Runs after Prisma push so tables are preserved)
# ============================================================================
vault-init:
image: ghcr.io/${GHCR_REPO:-heretek-ai/job-bot}/postgres:${IMAGE_TAG:-latest}
environment:
PGPASSWORD: ${POSTGRES_PASSWORD:-jobpilot_secret_password}
networks:
- app-net
depends_on:
postgres:
condition: service_healthy
db-migrate:
condition: service_completed_successfully
command:
- "psql"
- "-h"
- "postgres"
- "-U"
- "${POSTGRES_USER:-jobpilot}"
- "-d"
- "${POSTGRES_DB:-jobpilot}"
- "-v"
- "ON_ERROR_STOP=1"
- "-f"
- "/usr/local/share/jobbot/init-vault.sql"
restart: "no"
# ============================================================================
# 4. JobPilot API Backend (Bun + Elysia + Prisma)
# ============================================================================
api:
image: ghcr.io/${GHCR_REPO:-heretek-ai/job-bot}/api:${IMAGE_TAG:-latest}
restart: unless-stopped
env_file: .env
environment:
NODE_ENV: production
PORT: 4101
DATABASE_URL: ${DATABASE_URL:-postgresql://${POSTGRES_USER:-jobpilot}:${POSTGRES_PASSWORD:-jobpilot_secret_password}@postgres:5432/${POSTGRES_DB:-jobpilot}}
CORS_ORIGINS: ${CORS_ORIGINS:-http://localhost:8080,http://127.0.0.1:8080,http://localhost:4100,http://web:4100}
APP_URL: ${APP_URL:-http://localhost:8080}
ports:
- "127.0.0.1:${API_PORT:-4101}:4101"
volumes:
- api-storage:/app/apps/api/storage
- shared-workspace:/app/shared
networks:
- app-net
depends_on:
postgres:
condition: service_healthy
vault-init:
condition: service_completed_successfully
command:
- "sh"
- "-c"
- |
for var in SUPER_ADMIN_EMAIL GOOGLE_CLIENT_ID GOOGLE_CLIENT_SECRET GITHUB_CLIENT_ID GITHUB_CLIENT_SECRET RESEND_API_KEY VAPID_PUBLIC_KEY VAPID_PRIVATE_KEY VAPID_SUBJECT; do
eval "val=\$${var}"
if [ -z "$$val" ]; then unset "$$var"; fi
done
KEY_LEN=0
case "$${SECRET_MASTER_KEY:-}" in
""|REPLACE_*) KEY_LEN=0 ;;
*) KEY_LEN=$$(printf "%s" "$$SECRET_MASTER_KEY" | openssl base64 -d -A 2>/dev/null | wc -c | tr -d ' ') ;;
esac
if [ "$$KEY_LEN" != "32" ]; then
mkdir -p /app/apps/api/storage
if [ ! -s /app/apps/api/storage/.secret_master_key ]; then
openssl rand -base64 32 | tr -d '\r\n' > /app/apps/api/storage/.secret_master_key
fi
export SECRET_MASTER_KEY=$$(cat /app/apps/api/storage/.secret_master_key | tr -d '\r\n')
echo "[api] Auto-loaded persistent 32-byte SECRET_MASTER_KEY from storage"
fi
bun run db:seed || echo "[api] Warning: db:seed returned non-zero, continuing..."
exec bun run start
healthcheck:
test: ["CMD", "bun", "-e", "fetch('http://localhost:4101/api/health').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"]
interval: 20s
timeout: 5s
retries: 5
start_period: 20s
# ============================================================================
# 5. JobPilot Web UI (Prepackaged & Origin-Agnostic Next.js Standalone)
# ============================================================================
web:
image: ghcr.io/${GHCR_REPO:-heretek-ai/job-bot}/web:${IMAGE_TAG:-latest}
restart: unless-stopped
environment:
NODE_ENV: production
PORT: 4100
HOSTNAME: 0.0.0.0
INTERNAL_API_URL: http://api:4101
ports:
- "127.0.0.1:${WEB_PORT:-4100}:4100"
networks:
- app-net
depends_on:
api:
condition: service_healthy
healthcheck:
test: ["CMD", "node", "-e", "fetch('http://localhost:4100').then((r) => process.exit(r.ok ? 0 : 1), () => process.exit(1))"]
interval: 20s
timeout: 5s
retries: 3
start_period: 15s
# ============================================================================
# 6. Stealthy Auto-Browse: Camoufox + OS-Level Input + noVNC (:5900)
# ============================================================================
stealth-browser:
image: psyb0t/stealthy-auto-browse:latest
container_name: job-bot-stealth-browser-1
hostname: stealth-browser
restart: unless-stopped
shm_size: "2gb"
environment:
HTTP_LISTEN_PORT: 8080
VNC_LISTEN_PORT: 5900
VNC_SHOW_CURSOR: "true"
XVFB_RESOLUTION: ${XVFB_RESOLUTION:-1280x800}
TZ: ${TZ:-America/New_York}
PROXY_URL: ${BROWSER_PROXY_URL:-}
ports:
- "${VNC_PORT:-5900}:5900"
volumes:
- stealth-browser-data:/userdata
- shared-workspace:/app/shared
- api-storage:/app/apps/api/storage:ro
- terminal-data:/root/.jobpilot:ro
networks:
- browser-net
- app-net
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:8080/health || exit 1"]
interval: 15s
timeout: 5s
retries: 5
start_period: 20s
# ============================================================================
# 7. Prepackaged Autonomous Terminal Worker
# ============================================================================
terminal:
image: ghcr.io/${GHCR_REPO:-heretek-ai/job-bot}/terminal:${IMAGE_TAG:-latest}
restart: unless-stopped
shm_size: "1gb"
env_file: .env
environment:
DOTNET_EnableWriteXorExecute: "0"
PUBLIC_PORT: ${PUBLIC_PORT:-8080}
JOBPILOT_API: http://api:4101
JOBPILOT_WEB: http://web:4100
JOBPILOT_API_TOKEN: ${JOBPILOT_API_TOKEN:-}
DATABASE_URL: ${DATABASE_URL:-postgresql://${POSTGRES_USER:-jobpilot}:${POSTGRES_PASSWORD:-jobpilot_secret_password}@postgres:5432/${POSTGRES_DB:-jobpilot}}
STEALTH_BROWSER_URL: http://stealth-browser:8080
ANTHROPIC_BASE_URL: ${ANTHROPIC_BASE_URL:-https://opencode.ai/inference/anthropic}
ANTHROPIC_AUTH_TOKEN: ${ANTHROPIC_AUTH_TOKEN}
ANTHROPIC_DEFAULT_OPUS_MODEL: ${ANTHROPIC_DEFAULT_OPUS_MODEL:-claude-opus-5-5}
ANTHROPIC_DEFAULT_SONNET_MODEL: ${ANTHROPIC_DEFAULT_SONNET_MODEL:-claude-sonnet-5-5}
ANTHROPIC_DEFAULT_HAIKU_MODEL: ${ANTHROPIC_DEFAULT_HAIKU_MODEL:-claude-haiku-5-5}
ports:
- "127.0.0.1:${TERMINAL_PORT:-4102}:4102"
volumes:
- terminal-data:/root/.jobpilot
- claude-data:/root/.claude
- api-storage:/app/apps/api/storage
- shared-workspace:/app/shared
networks:
- app-net
- browser-net
depends_on:
api:
condition: service_healthy
stealth-browser:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://localhost:4102/healthz || exit 1"]
interval: 20s
timeout: 5s
retries: 5
start_period: 25s
# ============================================================================
# 8. Prepackaged Nginx Single-Origin Reverse Proxy (:8080)
# ============================================================================
nginx:
image: ghcr.io/${GHCR_REPO:-heretek-ai/job-bot}/nginx:${IMAGE_TAG:-latest}
restart: unless-stopped
ports:
- "${PUBLIC_PORT:-8080}:8080"
networks:
- app-net
depends_on:
web:
condition: service_healthy
api:
condition: service_healthy
terminal:
condition: service_healthy
healthcheck:
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1:8080/api/health || exit 1"]
interval: 20s
timeout: 5s
retries: 3
start_period: 10s
networks:
app-net:
driver: bridge
browser-net:
driver: bridge
volumes:
postgres-data:
api-storage:
stealth-browser-data:
terminal-data:
claude-data:
shared-workspace: