Initial commit: the self-hosting stack
Everything needed to run Jarvis on your own Docker host, and nothing else. The images are published; this is the compose that arranges them, the environment they read, and the prose explaining which values are load-bearing. It lives in its own repository rather than in a directory of the product's, because the audience is different in the one way that matters: a self-hoster has no access to the source and no reason to want it. Handing them a monorepo path to browse would be handing them a page of files they cannot clone, next to the four they can. WHAT IS HERE: - `docker-compose.yml` — postgres, redis, the api and the web. Only the web publishes a port; it reverse-proxies /api and the websocket internally, so a TLS terminator in front has exactly one target and the API is never reachable from outside the network. - `docker-compose.agent.yml` — the optional overlay that supplies the compiled agent binaries as a pullable image. Off by default, and the README says why leaving it off is a supported state rather than a broken one. - `.env.example` — every comment in it is load-bearing. The VAULT_MASTER_KEY note especially: it has no recovery, and a database backup does not protect what it wraps. - `.gitignore` — .env and database dumps, because the first thing anyone does with this repository is fill one of those with secrets and the second is to forget it is there. The README states the limitations plainly instead of leaving them to be discovered: SSH host keys are not verified, access tokens survive revocation for up to 15 minutes, self-registration is open by default and the first account created becomes super-admin, both containers run as root, and /api/health answers 200 while the database is down. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
# Jarvis — self-hosting stack.
|
||||
#
|
||||
# Everything runs from published images; nothing is built here and no source is needed.
|
||||
#
|
||||
# cp .env.example .env # then fill it in — see the comments in that file
|
||||
# docker compose pull
|
||||
# docker compose up -d
|
||||
#
|
||||
# Only the `web` service publishes a port. Its nginx serves the app and reverse-proxies /api and the
|
||||
# websocket to the internal `api` service, so your own TLS terminator has exactly one target and the
|
||||
# API is never reachable from outside this compose network.
|
||||
#
|
||||
# UPGRADING: `docker compose pull && docker compose up -d`. Both services track `latest` by default,
|
||||
# so that is the whole upgrade. Schema changes apply themselves when the api starts.
|
||||
#
|
||||
# `latest` does NOT mean the app forgets which build it is: the tag is a second name on the same image
|
||||
# as the version tag, and the version is stamped INTO the image at build time. The footer in the app and
|
||||
# /version.json keep reporting 0.51.0 or whatever you actually pulled.
|
||||
#
|
||||
# To pin instead — recommended once you are in production, because it makes an upgrade a decision rather
|
||||
# than a side effect of pulling — set JARVIS_IMAGE_API and JARVIS_IMAGE_WEB in .env to explicit tags.
|
||||
name: jarvis
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: jarvis
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
|
||||
POSTGRES_DB: jarvis
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U jarvis -d jarvis"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
# Append-only so a restart does not lose the queue and the socket fan-out state.
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
api:
|
||||
image: ${JARVIS_IMAGE_API:-git.luxit.be/luxit/jarvis-api:latest}
|
||||
restart: unless-stopped
|
||||
# The assistant runs long operations, and a deploy is the most common thing that interrupts one.
|
||||
# Given room to stop, the API aborts each loop, writes the partial answer with a note saying why
|
||||
# the transcript ends there, and marks the run interrupted so the next process picks it up.
|
||||
# Docker's 10s default is not enough. RUN_SHUTDOWN_GRACE_SEC must stay the smaller of the two.
|
||||
stop_grace_period: 60s
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
API_PORT: "4000"
|
||||
|
||||
# The address a browser reaches Jarvis on. This is the CORS decision: requests from any other
|
||||
# origin are refused, so a wrong value here presents as a sign-in page that cannot sign in.
|
||||
WEB_ORIGIN: ${WEB_ORIGIN:?set WEB_ORIGIN in .env}
|
||||
# The address baked into agent install commands and dialled by every enrolled machine. Usually
|
||||
# the same string as WEB_ORIGIN; separate because they answer different questions, and a wrong
|
||||
# value here does not fail at deploy time — it fails weeks later, on somebody else's server.
|
||||
PUBLIC_URL: ${PUBLIC_URL:-${WEB_ORIGIN}}
|
||||
|
||||
DATABASE_URL: postgresql://jarvis:${POSTGRES_PASSWORD}@postgres:5432/jarvis?schema=public
|
||||
REDIS_URL: redis://redis:6379
|
||||
|
||||
JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET:?set JWT_ACCESS_SECRET in .env}
|
||||
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:?set JWT_REFRESH_SECRET in .env}
|
||||
JWT_ACCESS_TTL: "900"
|
||||
JWT_REFRESH_TTL: "1209600"
|
||||
|
||||
# READ THE NOTE IN .env.example BEFORE CHANGING THIS. Every credential in the vault is
|
||||
# encrypted under it; lose it and they are gone, with no recovery of any kind.
|
||||
VAULT_MASTER_KEY: ${VAULT_MASTER_KEY:?set VAULT_MASTER_KEY in .env}
|
||||
|
||||
# Any OpenAI-compatible endpoint. Defaults to OpenAI itself.
|
||||
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:?set OPENAI_API_KEY in .env}
|
||||
OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4o}
|
||||
OPENAI_THINKING_LEVEL: ${OPENAI_THINKING_LEVEL:-medium}
|
||||
|
||||
# How many proxies sit in front and rewrite X-Forwarded-For. ONE is the web container's own
|
||||
# nginx, which is always there — so 1 is right when nothing else fronts it, and 2 when your own
|
||||
# TLS terminator does. Raising it is the dangerous direction: the API trusts that many hops of a
|
||||
# header the client can forge, and too high lets a caller choose the IP that lands in the audit
|
||||
# log, in the session list and in the enrollment rate limit.
|
||||
TRUST_PROXY_HOPS: ${TRUST_PROXY_HOPS:-2}
|
||||
|
||||
AGENT_HEARTBEAT_INTERVAL_SEC: ${AGENT_HEARTBEAT_INTERVAL_SEC:-30}
|
||||
RUN_SHUTDOWN_GRACE_SEC: ${RUN_SHUTDOWN_GRACE_SEC:-25}
|
||||
|
||||
# Where the agent binaries live, if you have them. Leaving this unset is a supported state:
|
||||
# everything except the agent installer works, and the installer answers 503 saying no build is
|
||||
# published. See the README — a self-hosted instance has no way to produce these.
|
||||
AGENT_RELEASE_DIR: ${AGENT_RELEASE_DIR:-}
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- "fetch('http://localhost:4000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
# First boot syncs the schema and runs the data backfills before it listens.
|
||||
start_period: 40s
|
||||
|
||||
web:
|
||||
image: ${JARVIS_IMAGE_WEB:-git.luxit.be/luxit/jarvis-web:latest}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- api
|
||||
ports:
|
||||
# Put your own TLS terminator in front of this. Jarvis speaks plain HTTP here on purpose and
|
||||
# reads X-Forwarded-Proto to know what the browser actually used.
|
||||
- "${JARVIS_PORT:-8080}:80"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
Reference in New Issue
Block a user