Files
insforge--insforge/docker-compose.prod.yml
Lyu 526e0f280a feat(compute): read the Docker socket's group instead of asking for it
Enabling Docker took three steps because the production image runs as `node` and
the socket is mode 660 root:docker, so a matching group was required. The group id
is host-specific — 0 on Docker Desktop, commonly 999 on Debian/Ubuntu, 993 on
Amazon Linux 2023 — and a wrong value is a silent EACCES that shows up as "Docker
never appears in the dashboard". There is no value that is right everywhere, so
there is no default to ship.

The container can read it off the socket. The runner stage no longer sets
`USER node`; a new entrypoint starts as root, takes the socket's group with
`stat -c %g`, joins it, and execs the command through `su-exec node`. The app
process is still `node` and `tini` is still PID 1. With no socket mounted it
drops to `node` and does nothing else, and a deployment that pins `user:` is
left alone.

`group_add` and `DOCKER_GID` are gone from all five compose files, `.env.example`,
the docs in four languages, the dashboard guide (three steps to two) and the
not-configured API message. Uncommenting one volume line is now the whole edit.

The mount stays opt-in: the socket is root-equivalent on the host, and on by
default would hand every deployment's backend the ability to take over its host
whether or not it uses compute.

Verified on a build of `target: runner` — no socket: uid 1000; socket at gid 0:
joins root, writable; socket at gid 999 with the group already present: joins it,
writable; and the old `USER node` path with no entrypoint: denied, which is the
failure the removed step existed to avoid. Then end to end from that image
against a live database: provider ready, metadata slice present, a service
created and serving on its published port.

Also batches the compute logs limiter test. 120 sequential requests overran the
10s timeout on a busy machine and 120 simultaneous ones had connections dropped;
ten in flight is neither, and the assertion is by count rather than position.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 16:48:56 -07:00

258 lines
11 KiB
YAML

services:
postgres:
image: ghcr.io/insforge/postgres:v15.13.4
command: postgres -c config_file=/etc/postgresql/postgresql.conf -c app.encryption_key='${ENCRYPTION_KEY:-${JWT_SECRET:-dev-secret-please-change-in-production}}'
environment:
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
- POSTGRES_DB=${POSTGRES_DB:-insforge}
volumes:
- postgres-data:/var/lib/postgresql/data
- ./deploy/docker-init/db/db-init.sql:/docker-entrypoint-initdb.d/01-init.sql
- ./deploy/docker-init/db/jwt.sql:/docker-entrypoint-initdb.d/02-jwt.sql
- ./deploy/docker-init/db/postgresql.conf:/etc/postgresql/postgresql.conf
ports:
- "127.0.0.1:${POSTGRES_PORT:-5432}:5432"
networks:
- insforge-network
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
interval: 5s
timeout: 5s
retries: 5
security_opt:
- no-new-privileges:true
postgrest:
image: postgrest/postgrest:v12.2.12
restart: unless-stopped
security_opt:
- no-new-privileges:true
environment:
#POSTGRES_USER: ${POSTGRES_USER:-postgres}
#POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-postgres}
#POSTGRES_DB: ${POSTGRES_DB:-insforge}
PGRST_DB_URI: postgres://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-insforge}
PGRST_OPENAPI_SERVER_PROXY_URI: http://localhost:3000
PGRST_DB_SCHEMA: public
PGRST_DB_ANON_ROLE: anon
# Keep in sync with the backend's POSTGREST_MAX_SOCKETS (default 50)
PGRST_DB_POOL: ${PGRST_DB_POOL:-50}
PGRST_JWT_SECRET: ${JWT_SECRET:-dev-secret-please-change-in-production}
ports:
- "127.0.0.1:${POSTGREST_PORT:-5430}:3000"
depends_on:
postgres:
condition: service_healthy
networks:
- insforge-network
insforge:
build:
context: .
dockerfile: Dockerfile
target: runner
args:
VITE_API_BASE_URL: ${VITE_API_BASE_URL:-http://localhost:7130}
VITE_PUBLIC_POSTHOG_KEY: ${VITE_PUBLIC_POSTHOG_KEY:-}
depends_on:
postgres:
condition: service_healthy
ports:
- "${APP_PORT:-7130}:7130"
- "127.0.0.1:${AUTH_PORT:-7131}:7131"
environment:
- PORT=7130
- PROJECT_ROOT=/app
- API_BASE_URL=${API_BASE_URL:-}
- JWT_SECRET=${JWT_SECRET:-dev-secret-please-change-in-production}
- ENCRYPTION_KEY=${ENCRYPTION_KEY:-}
- ROOT_ADMIN_USERNAME=${ROOT_ADMIN_USERNAME:-${ADMIN_EMAIL:-admin}}
- ROOT_ADMIN_PASSWORD=${ROOT_ADMIN_PASSWORD:-${ADMIN_PASSWORD:-change-this-password}}
- ADMIN_EMAIL=${ROOT_ADMIN_USERNAME:-${ADMIN_EMAIL:-admin}}
- ADMIN_PASSWORD=${ROOT_ADMIN_PASSWORD:-${ADMIN_PASSWORD:-change-this-password}}
- AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID:-}
- AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY:-}
# PostgreSQL connection
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- POSTGRES_DB=${POSTGRES_DB:-insforge}
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
- DATABASE_URL=postgresql://${POSTGRES_USER:-postgres}:${POSTGRES_PASSWORD:-postgres}@postgres:5432/${POSTGRES_DB:-insforge}
- POSTGREST_BASE_URL=http://postgrest:3000
- POSTGREST_MAX_SOCKETS=${POSTGREST_MAX_SOCKETS:-}
- POSTGREST_MAX_FREE_SOCKETS=${POSTGREST_MAX_FREE_SOCKETS:-}
- POSTGREST_FREE_SOCKET_TIMEOUT_MS=${POSTGREST_FREE_SOCKET_TIMEOUT_MS:-}
# Deno Runtime URL for serverless functions
- DENO_RUNTIME_URL=http://deno:7133
- DENO_DEPLOY_TOKEN=${DENO_DEPLOY_TOKEN:-}
- DENO_DEPLOY_ORG_ID=${DENO_DEPLOY_ORG_ID:-}
# Storage Configuration
- MAX_FILE_SIZE=${MAX_FILE_SIZE:-}
- MAX_JSON_BODY_SIZE=${MAX_JSON_BODY_SIZE:-}
- MAX_URLENCODED_BODY_SIZE=${MAX_URLENCODED_BODY_SIZE:-}
- KEEP_ALIVE_TIMEOUT_MS=${KEEP_ALIVE_TIMEOUT_MS:-}
# S3-compatible storage (MinIO, RustFS, Wasabi, Tencent COS, Aliyun OSS,
# R2 ...). S3_BUCKET/S3_REGION preferred; AWS_* are the legacy names
# kept for cloud provisioning and existing .env files.
- S3_BUCKET=${S3_BUCKET:-}
- S3_REGION=${S3_REGION:-}
- AWS_S3_BUCKET=${AWS_S3_BUCKET:-}
- AWS_REGION=${AWS_REGION:-}
- S3_ENDPOINT_URL=${S3_ENDPOINT_URL:-}
- S3_ACCESS_KEY_ID=${S3_ACCESS_KEY_ID:-}
- S3_SECRET_ACCESS_KEY=${S3_SECRET_ACCESS_KEY:-}
- S3_FORCE_PATH_STYLE=${S3_FORCE_PATH_STYLE:-}
- S3_MAX_OBJECT_SIZE_BYTES=${S3_MAX_OBJECT_SIZE_BYTES:-}
- S3_USE_PRESIGNED_URLS=${S3_USE_PRESIGNED_URLS:-}
- AWS_CLOUDFRONT_URL=${AWS_CLOUDFRONT_URL:-}
- AWS_CLOUDFRONT_KEY_PAIR_ID=${AWS_CLOUDFRONT_KEY_PAIR_ID:-}
- AWS_CLOUDFRONT_PRIVATE_KEY=${AWS_CLOUDFRONT_PRIVATE_KEY:-}
# Deployment Configuration
- VERCEL_TOKEN=${VERCEL_TOKEN:-}
- VERCEL_TEAM_ID=${VERCEL_TEAM_ID:-}
- VERCEL_PROJECT_ID=${VERCEL_PROJECT_ID:-}
# Multi-tenant Cloud Configuration
- DEPLOYMENT_ID=${DEPLOYMENT_ID:-}
- PROJECT_ID=${PROJECT_ID:-}
- APP_KEY=${APP_KEY:-}
- ACCESS_API_KEY=${ACCESS_API_KEY:-}
- ACCESS_ANON_KEY=${ACCESS_ANON_KEY:-}
# LLM Model API keys
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
# Stripe Payments Configuration
- STRIPE_TEST_SECRET_KEY=${STRIPE_TEST_SECRET_KEY:-}
- STRIPE_LIVE_SECRET_KEY=${STRIPE_LIVE_SECRET_KEY:-}
# OAuth Configuration
- GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID:-}
- GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET:-}
- GITHUB_CLIENT_ID=${GITHUB_CLIENT_ID:-}
- GITHUB_CLIENT_SECRET=${GITHUB_CLIENT_SECRET:-}
- DISCORD_CLIENT_ID=${DISCORD_CLIENT_ID:-}
- DISCORD_CLIENT_SECRET=${DISCORD_CLIENT_SECRET:-}
- MICROSOFT_CLIENT_ID=${MICROSOFT_CLIENT_ID:-}
- MICROSOFT_CLIENT_SECRET=${MICROSOFT_CLIENT_SECRET:-}
- LINKEDIN_CLIENT_ID=${LINKEDIN_CLIENT_ID:-}
- LINKEDIN_CLIENT_SECRET=${LINKEDIN_CLIENT_SECRET:-}
- X_CLIENT_ID=${X_CLIENT_ID:-}
- X_CLIENT_SECRET=${X_CLIENT_SECRET:-}
- APPLE_CLIENT_ID=${APPLE_CLIENT_ID:-}
- APPLE_CLIENT_SECRET=${APPLE_CLIENT_SECRET:-}
# Logs directory
- LOGS_DIR=/insforge-logs
# Anonymous telemetry (set INSFORGE_TELEMETRY_DISABLED=1 to opt out)
- INSFORGE_TELEMETRY_DISABLED=${INSFORGE_TELEMETRY_DISABLED:-}
- INSFORGE_DEPLOYMENT_METHOD=docker-compose
# Storage directory (for local file storage when S3 is not configured)
- STORAGE_DIR=/insforge-storage
# ─── Custom Compute: Docker provider (opt-in) ──────────────────────────
# Mounting the Docker socket below is what enables it: the driver registers
# itself when the socket is reachable, so nothing runs containers on your
# host unless you deliberately grant this.
#
# Read the security note in the compute docs first. The driver constructs
# every container spec itself and never forwards caller-supplied options, so
# a leaked API key cannot request a privileged container — but the socket is
# still root-equivalent on the host.
- COMPUTE_PROVIDER=${COMPUTE_PROVIDER:-}
# Socket the driver dials, mounted at the same path below so both sides
# agree. Override for rootless Docker or Podman.
- DOCKER_SOCKET_PATH=${DOCKER_SOCKET_PATH:-}
# Ingress default for services that do not choose one. `none` publishes no
# host port, which is right for workers and processors.
- COMPUTE_DEFAULT_INGRESS=${COMPUTE_DEFAULT_INGRESS:-none}
# Host address used to build URLs for `port` ingress. Unset, no URL is
# advertised rather than guessing an address that may not resolve.
- COMPUTE_PUBLIC_HOST=${COMPUTE_PUBLIC_HOST:-}
# Base domain for `host` ingress. Unset, a service that asks for a
# hostname gets no URL rather than an unroutable guess.
- COMPUTE_DOMAIN=${COMPUTE_DOMAIN:-}
# Bind address for published ports. Loopback by default — reaching a
# container from the internet should be a deliberate choice.
- COMPUTE_BIND_ADDRESS=${COMPUTE_BIND_ADDRESS:-127.0.0.1}
# Ceiling on an uploaded source-build context. Express buffers the whole
# tarball before the handler runs, so this is a memory bound, not just a
# policy — lower it on a small host.
- COMPUTE_BUILD_MAX_CONTEXT=${COMPUTE_BUILD_MAX_CONTEXT:-}
# Seconds an upload may send nothing before it is treated as stalled and
# cut loose. Only one build runs at a time, so a connection that stops
# making progress would otherwise block every other deploy. Resets on each
# chunk, so a slow but active link is never cut.
- COMPUTE_BUILD_UPLOAD_IDLE_TIMEOUT=${COMPUTE_BUILD_UPLOAD_IDLE_TIMEOUT:-}
# Keep compute containers off this project's network. Off by default:
# proximity to the database and storage is the point.
- COMPUTE_ISOLATE_NETWORK=${COMPUTE_ISOLATE_NETWORK:-}
volumes:
- storage-data:/insforge-storage
- shared-logs:/insforge-logs
# Uncomment to enable the Docker compute provider. The socket is
# root-equivalent on the host, so it is opt-in rather than on by default.
# Nothing else to set: the entrypoint reads the socket's group and joins it
# before dropping to the app user.
# - ${DOCKER_SOCKET_PATH:-/var/run/docker.sock}:${DOCKER_SOCKET_PATH:-/var/run/docker.sock}
restart: unless-stopped
networks:
- insforge-network
security_opt:
- no-new-privileges:true
# Deno serverless runtime for edge functions
deno:
image: denoland/deno:alpine-2.0.6
working_dir: /app
security_opt:
- no-new-privileges:true
depends_on:
- postgres
- postgrest
ports:
- "127.0.0.1:${DENO_PORT:-7133}:7133"
environment:
- PORT=7133
- DENO_ENV=${DENO_ENV:-production}
- DENO_DIR=/deno-dir
# PostgreSQL connection
- POSTGRES_HOST=postgres
- POSTGRES_PORT=5432
- POSTGRES_DB=${POSTGRES_DB:-insforge}
- POSTGRES_USER=${POSTGRES_USER:-postgres}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-postgres}
- POSTGREST_BASE_URL=http://postgrest:3000
# Worker timeout (60 seconds default)
- WORKER_TIMEOUT_MS=${WORKER_TIMEOUT_MS:-60000}
# Encryption keys for decrypting function secrets
- ENCRYPTION_KEY=${ENCRYPTION_KEY}
- JWT_SECRET=${JWT_SECRET:-dev-secret-please-change-in-production}
volumes:
- ./functions:/app/functions
- deno_cache:/deno-dir
command: >
sh -c "
echo 'Downloading Deno dependencies...' &&
deno cache functions/server.ts &&
echo 'Starting Deno server on port 7133...' &&
deno run --unstable-worker-options --allow-net --allow-env --allow-read=./functions/worker-template.js functions/server.ts
"
restart: unless-stopped
networks:
- insforge-network
volumes:
postgres-data:
driver: local
deno_cache:
driver: local
storage-data:
driver: local
shared-logs:
driver: local
networks:
insforge-network:
driver: bridge