Sync the self-hosting stack

This commit is contained in:
2026-08-24 13:02:29 +02:00
parent 4fa9cabefb
commit 0674e550f1
4 changed files with 818 additions and 108 deletions
+117 -19
View File
@@ -2,10 +2,21 @@
#
# 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
#
# That is the whole first deployment. Open the address in a browser and an install screen asks the
# questions: it creates the first administrator, checks what this deployment actually looks like
# from inside, and stores the address, the model and the rest as settings you can change later.
#
# THERE IS NO .env STEP ANY MORE. This file used to demand six variables before it would start,
# four of them secrets you had to generate with `openssl` — and it failed badly when one was wrong,
# because the API exited 1 and the restart policy turned a typo into a crash loop. `.env.example`
# still exists and every value in it is optional: pinning, the host port, the agent overlay.
#
# THE ONE THING TO DO AFTERWARDS: back up the `jarvis_secrets` volume, separately from the database.
# The install screen shows you why and will not finish until you have. See the `init` service below.
#
# 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.
@@ -30,15 +41,53 @@
name: jarvis
services:
# Writes the stack's secrets, once, into a volume the other services read. Runs to completion
# before anything else starts and then exits — `docker compose ps` shows it as `Exited (0)`, which is
# what success looks like and not something to fix.
#
# THIS IS WHY .env NO LONGER ASKS FOR FOUR openssl INVOCATIONS. A vault key, two signing secrets
# and a database password are values no person should be choosing, and asking for them put the
# most consequential one — the vault key, which everything in the vault is sealed under — in the
# hands of whoever was least equipped to look after it, at the moment they were least interested.
#
# It never overwrites. On an upgrade it ADOPTS whatever is still in your .env, so a stack that has
# been running for a year keeps its own keys and this service is inert from its second run onward.
#
# BACK THIS VOLUME UP, AND SEPARATELY FROM THE DATABASE. It is deliberately not `postgres_data`:
# every credential in the vault is encrypted under a key that lives here, so a database dump that
# travelled with its own key would be a dump that decrypts itself. The setup screen shows you the
# key once and will not let you past until you have put it somewhere.
init:
image: ${JARVIS_IMAGE_API:-git.luxit.be/luxit/jarvis-api:stable}
entrypoint: ["node", "/app/apps/api/init-secrets.cjs"]
restart: "no"
environment:
# Only read when the corresponding file does not exist yet — the upgrade path for a stack
# whose .env already holds these. Empty on a fresh install, which is the ordinary case now.
VAULT_MASTER_KEY: ${VAULT_MASTER_KEY:-}
JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET:-}
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:-}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-}
volumes:
- jarvis_secrets:/var/lib/jarvis/secrets
postgres:
image: postgres:16-alpine
restart: unless-stopped
depends_on:
init:
condition: service_completed_successfully
environment:
POSTGRES_USER: jarvis
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
# The file, never the variable. The two are mutually exclusive in this image — it refuses to
# start when both are set — which is exactly why `init` adopts an existing .env value into the
# file instead of the compose file trying to choose between them.
POSTGRES_PASSWORD_FILE: /var/lib/jarvis/secrets/postgres-password
POSTGRES_DB: jarvis
volumes:
- postgres_data:/var/lib/postgresql/data
# Read-only: postgres consumes this secret and has no business ever writing one.
- jarvis_secrets:/var/lib/jarvis/secrets:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U jarvis -d jarvis"]
interval: 5s
@@ -71,33 +120,58 @@ services:
condition: service_healthy
redis:
condition: service_healthy
init:
condition: service_completed_successfully
volumes:
# Read-only. The api reads these keys and must never be the thing that creates one: a
# container that could write here is a container whose bug can replace the key the vault is
# sealed under, and nothing about that failure is visible until somebody opens a credential.
- jarvis_secrets:/var/lib/jarvis/secrets:ro
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}}
# A SEED, not a requirement. The install screen asks for this address and pre-fills it with the
# one you are reading the page at; whatever is stored wins from then on. Passed through so a
# stack that already set it upgrades with its origin intact, and empty on a fresh install —
# which is a supported state, not a broken one.
#
# PUBLIC_URL is deliberately no longer passed. It was a second variable for the same address,
# and every consumer now reads the single stored value.
WEB_ORIGIN: ${WEB_ORIGIN:-}
DATABASE_URL: postgresql://jarvis:${POSTGRES_PASSWORD}@postgres:5432/jarvis?schema=public
# Assembled by the entrypoint from POSTGRES_PASSWORD_FILE, because a password that lives in a
# file cannot be interpolated into a URL by compose. Set DATABASE_URL in .env to override it
# outright — pointing at a managed postgres outside this stack, say.
DATABASE_URL: ${DATABASE_URL:-}
POSTGRES_PASSWORD_FILE: /var/lib/jarvis/secrets/postgres-password
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"
# Delivered as files rather than values, by the `init` service above. The `_FILE` suffix is the
# convention this postgres image and most others already use, so a `docker secret` of your own
# can be pointed at these paths instead with nothing here changing.
JWT_ACCESS_SECRET_FILE: /var/lib/jarvis/secrets/jwt-access-secret
JWT_REFRESH_SECRET_FILE: /var/lib/jarvis/secrets/jwt-refresh-secret
# Interpolated like every other tuning value, rather than frozen here. These two were the only
# ones written as literals, which meant the one knob an operator under a session policy asks
# for was also the one they could not reach from `.env`. See `.env.example` for what each
# actually does — the refresh one is not the session lifetime it looks like.
JWT_ACCESS_TTL: ${JWT_ACCESS_TTL:-900}
JWT_REFRESH_TTL: ${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}
# THE ONE YOU CANNOT LOSE. Every credential in the vault is encrypted under it, and so is this
# instance's licence identity, every authenticator secret and every terminal recording. It is
# generated into `jarvis_secrets` on your first boot and exists NOWHERE ELSE — back that volume
# up separately from the database. The setup screen shows it to you once.
VAULT_MASTER_KEY_FILE: /var/lib/jarvis/secrets/vault-master-key
# Any OpenAI-compatible endpoint. Defaults to OpenAI itself.
# Any OpenAI-compatible endpoint. All four of these are seeds for the stored settings.
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1}
OPENAI_API_KEY: ${OPENAI_API_KEY:?set OPENAI_API_KEY in .env}
# NO LONGER REQUIRED, and that is the point of the change. This used to be validated at boot,
# so a key that was merely wrong made the API exit 1 and the restart policy turned it into a
# crash loop. The install screen asks for it, tests it against the provider, and stores it
# encrypted; what is passed here only seeds an instance that has nothing stored yet.
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4o}
OPENAI_THINKING_LEVEL: ${OPENAI_THINKING_LEVEL:-medium}
@@ -111,6 +185,20 @@ services:
# Required to create new organizations, users or agents. Without it an instance keeps running
# everything it already has, creates nothing new, and contacts nobody. See the README.
JARVIS_LICENSE_KEY: ${JARVIS_LICENSE_KEY:-}
# WHETHER ANYTHING LEAVES THIS NETWORK, and it has to be passed explicitly — this line is the
# whole control. It was missing until 24 August 2026, and its absence made a published promise
# false: `.env` is read by compose for `${...}` INTERPOLATION ONLY, never injected into a
# container, and no `env_file` makes up the difference. So an operator who wrote
# `JARVIS_TELEMETRY=off` in `.env` — exactly as the README, the changelog and `.env.example` all
# told them to — was reporting to the publisher the entire time, because the api saw nothing
# and applied its default of "on".
#
# Defaulted to the word rather than to empty, deliberately. The schema defaults an ABSENT
# value to "on", but `${VAR:-}` passes an empty STRING, which is present — the same trap that
# killed WEB_ORIGIN on a zero-config boot. Passing "on" means the container never has to
# reason about the difference.
JARVIS_TELEMETRY: ${JARVIS_TELEMETRY:-on}
# Which distribution channel this instance follows, shown to signed-in operators beside the
# version numbers. Set `JARVIS_CHANNEL=stable` (or `dev`) in .env if you track a channel;
# LEAVE IT EMPTY IF YOU PIN EXACT VERSIONS, because then you follow no channel — you follow a
@@ -121,6 +209,13 @@ services:
# one you are on.
APP_CHANNEL: ${JARVIS_CHANNEL:-}
# Install from the environment and never show the wizard. For fleets and for CI — see
# .env.example. Off means the ordinary install screen, which is what one deployment wants.
JARVIS_UNATTENDED: ${JARVIS_UNATTENDED:-off}
JARVIS_ADMIN_EMAIL: ${JARVIS_ADMIN_EMAIL:-}
JARVIS_ADMIN_PASSWORD: ${JARVIS_ADMIN_PASSWORD:-}
JARVIS_ADMIN_NAME: ${JARVIS_ADMIN_NAME:-}
AGENT_HEARTBEAT_INTERVAL_SEC: ${AGENT_HEARTBEAT_INTERVAL_SEC:-30}
RUN_SHUTDOWN_GRACE_SEC: ${RUN_SHUTDOWN_GRACE_SEC:-25}
@@ -153,3 +248,6 @@ services:
volumes:
postgres_data:
redis_data:
# The keys, deliberately apart from postgres_data. Back it up, and not to the same place: a
# database dump is worthless to a thief without this, and worthless to YOU without it either.
jarvis_secrets: