Sync the self-hosting stack
This commit is contained in:
+146
-56
@@ -1,81 +1,146 @@
|
||||
# Jarvis — configuration. Copy to `.env` beside the compose file and fill in.
|
||||
# Jarvis — optional configuration.
|
||||
#
|
||||
# cp .env.example .env
|
||||
# THERE IS NOTHING YOU HAVE TO FILL IN. You do not need this file at all:
|
||||
#
|
||||
# Nothing here has a safe placeholder value: the four secrets must be generated, and the two URLs
|
||||
# must be yours. The compose file refuses to start rather than booting with something wrong.
|
||||
# docker compose up -d
|
||||
#
|
||||
# then open the address in a browser and the install screen asks the questions. It creates the first
|
||||
# administrator, checks the deployment, and stores the address, the model and everything else as
|
||||
# settings you can change later from a screen.
|
||||
#
|
||||
# THIS FILE USED TO BE MANDATORY, and that is the change. It asked for six values before the
|
||||
# container would start, four of them secrets to be generated with `openssl` — and it failed in the
|
||||
# worst available way, because an invalid one made the API exit 1 and the restart policy turned a
|
||||
# typo into a crash loop, with the reason on line 40 of a log nobody had a reason to open.
|
||||
#
|
||||
# What is left below is for deployments that prefer to state things in writing: pinning image
|
||||
# versions, running the agent overlay, choosing a host port. Every value here is optional, and the
|
||||
# ones that overlap with the install screen SEED it — they are read once, when nothing is stored
|
||||
# yet, and never again. Change them afterwards and nothing happens; change the setting instead.
|
||||
#
|
||||
# cp .env.example .env # only if you want any of this
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Where your instance lives
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# The address a browser reaches Jarvis on, scheme included and no trailing slash.
|
||||
#
|
||||
# This is the CORS decision. It accepts exactly ONE origin — not a list — and every request from
|
||||
# anywhere else is refused, which presents as a sign-in page that cannot sign in.
|
||||
WEB_ORIGIN=https://jarvis.example.com
|
||||
|
||||
# The address enrolled machines dial. Defaults to WEB_ORIGIN, which is right for a single-origin
|
||||
# deployment. Set it separately only if the app and the API answer on different hostnames.
|
||||
#PUBLIC_URL=https://jarvis.example.com
|
||||
|
||||
# Host port the web container publishes. Put your TLS terminator in front of it.
|
||||
#
|
||||
# The one value on this page that genuinely cannot move to a screen: it is a Docker fact, decided
|
||||
# before anything in the application is running.
|
||||
JARVIS_PORT=8080
|
||||
|
||||
# The address a browser reaches Jarvis on. ASKED BY THE INSTALL SCREEN, which pre-fills it with the
|
||||
# address you are already reading it at — so setting it here is only useful if you want it stated in
|
||||
# writing, or if you are upgrading a stack that already had it.
|
||||
#
|
||||
# It decides CORS, what enrolled machines dial, the base of every invitation link, and the WebAuthn
|
||||
# relying party. Changing it later orphans every passkey already enrolled, so the screen warns you.
|
||||
#WEB_ORIGIN=https://jarvis.example.com
|
||||
|
||||
# PUBLIC_URL IS GONE. It was a second address for the same thing, kept separate for a split-hostname
|
||||
# deployment nobody ran; the install screen asks once and every consumer reads that one value. An
|
||||
# instance that still sets it is not broken — it is simply ignored.
|
||||
|
||||
# How many proxies rewrite X-Forwarded-For before a request reaches the API.
|
||||
#
|
||||
# One is the web container's own nginx, which is always there. So: 1 if nothing else fronts it, 2 if
|
||||
# your own reverse proxy does — which is the usual case and the default. Raising it is the dangerous
|
||||
# direction, because the API then trusts that many hops of a header the client can forge, and a caller
|
||||
# can choose the address that lands in the audit log and in the session list.
|
||||
TRUST_PROXY_HOPS=2
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Secrets — generate every one of these, never copy them from anywhere
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Postgres. Only ever used inside the compose network.
|
||||
# openssl rand -base64 24
|
||||
POSTGRES_PASSWORD=
|
||||
|
||||
# Session signing. Two different values, at least 16 characters each.
|
||||
# openssl rand -base64 48
|
||||
JWT_ACCESS_SECRET=
|
||||
JWT_REFRESH_SECRET=
|
||||
|
||||
# THE ONE YOU CANNOT LOSE.
|
||||
# ASKED BY THE INSTALL SCREEN, which is the only place this can honestly be answered: it shows you
|
||||
# the header chain your own request actually carried and the address the API resolved from it, and
|
||||
# you confirm what you see. Nobody knows this number in advance.
|
||||
#
|
||||
# Must decode to exactly 32 bytes:
|
||||
# openssl rand -base64 32
|
||||
#
|
||||
# Every credential in the vault — SSH keys, API secrets, Microsoft 365 client secrets — is encrypted
|
||||
# under this key with AES-256-GCM, and so is everything Jarvis seals OUTSIDE the vault: the Microsoft
|
||||
# Graph client secret it sends its own mail with, this instance's licence identity key, every
|
||||
# authenticator-app secret, and every terminal recording. It is not stored anywhere but here.
|
||||
#
|
||||
# Change it or lose it and none of that data can be read again, by you or by anyone. A database backup
|
||||
# does not save you: the backup holds the ciphertext. Back this value up separately from the database,
|
||||
# somewhere you would still have it if this host were gone. An instance whose key has changed keeps
|
||||
# LOOKING configured — the rows are all there — and fails on every reveal.
|
||||
VAULT_MASTER_KEY=
|
||||
# One is the web container's own nginx, which is always there; two if your own reverse proxy fronts
|
||||
# it, which is the usual case. Raising it past the real number is the dangerous direction, because
|
||||
# the API then trusts that many hops of a header the client can forge — and a caller can choose the
|
||||
# address that lands in your audit log and in the session list.
|
||||
#TRUST_PROXY_HOPS=2
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The model
|
||||
# Secrets — there is nothing to fill in here any more
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# This section used to hold four values and four `openssl rand` invocations. They are generated for
|
||||
# you now, on your first `docker compose up`, by the `init` service — into the `jarvis_secrets` volume,
|
||||
# where the database password, the two session-signing secrets and the vault master key live.
|
||||
#
|
||||
# WHY THEY LEFT THIS FILE. A vault key is not a preference. Asking for one put the single most
|
||||
# consequential value in the deployment — everything in the vault is encrypted under it — in front of
|
||||
# whoever was least equipped to look after it, at the moment they were least interested in it, in a
|
||||
# file they were trying to get through. Nobody ever chose a better key than `openssl rand` would have.
|
||||
#
|
||||
# WHAT YOU STILL HAVE TO DO, AND IT IS THE IMPORTANT ONE:
|
||||
#
|
||||
# BACK UP THE `jarvis_secrets` VOLUME, SEPARATELY FROM THE DATABASE.
|
||||
#
|
||||
# The vault key is in there and NOWHERE ELSE. A database backup does not save you — the backup holds
|
||||
# the ciphertext. An instance whose key is gone keeps LOOKING configured, with every row in place,
|
||||
# and fails on every reveal. It is a separate volume from `postgres_data` precisely so that the two
|
||||
# can be, and must be, backed up to different places: a dump that travelled with the key that opens
|
||||
# it is a dump that opens itself.
|
||||
#
|
||||
# The setup screen shows you the key once, on your first visit, and will not let you finish until you
|
||||
# have put it somewhere. That is the moment to do this.
|
||||
#
|
||||
# UPGRADING FROM A .env THAT ALREADY HAS THESE? Leave them exactly where they are. The generator
|
||||
# ADOPTS an existing value rather than replacing it, and never overwrites a secret it has already
|
||||
# written — so your instance keeps its own keys and nothing about your vault changes. You can delete
|
||||
# them from this file once the stack has come up once, or leave them; they are read only when the
|
||||
# corresponding file does not exist yet.
|
||||
#
|
||||
# Pointing at secrets of your own instead? Every one of them also accepts a `_FILE` variable
|
||||
# (VAULT_MASTER_KEY_FILE, JWT_ACCESS_SECRET_FILE, …), which is the convention the postgres image and
|
||||
# most others already use — so a `docker secret` can be mounted at those paths with nothing else
|
||||
# changing.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The model — asked by the install screen, with a test button
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# All four moved. The install screen asks for the endpoint and the key, lists the models the endpoint
|
||||
# actually serves, and ASKS THE MODEL FOR A TOKEN before it saves anything — so a key that is
|
||||
# well-formed and wrong, an account out of credit or a model id the provider does not serve is a
|
||||
# sentence on screen instead of a fault in somebody's first conversation.
|
||||
#
|
||||
# The key is stored encrypted under this instance's vault key, exactly like the outbound mail secret,
|
||||
# and is never shown again. Change any of it later under Settings -> Platform -> Model.
|
||||
#
|
||||
# Setting them here still works and seeds an instance that has nothing stored — for a deployment
|
||||
# that would rather state its model in a file. They are read once and never again.
|
||||
|
||||
# Any OpenAI-compatible endpoint: OpenAI, a self-hosted gateway, a local server.
|
||||
#OPENAI_BASE_URL=https://api.openai.com/v1
|
||||
|
||||
# Required. Jarvis will not start without it.
|
||||
OPENAI_API_KEY=
|
||||
|
||||
# Must be a model your endpoint actually serves, and it should be a good one — this model is deciding
|
||||
# what to run on production infrastructure.
|
||||
#OPENAI_API_KEY=
|
||||
#OPENAI_MODEL=gpt-4o
|
||||
|
||||
# minimal | low | medium | high. Higher costs latency and tokens and is worth it for real work.
|
||||
#OPENAI_THINKING_LEVEL=medium
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Unattended installation (for fleets and for CI)
|
||||
# ---------------------------------------------------------------------------
|
||||
#
|
||||
# Everything above is optional because the install screen asks for it. This is the other direction:
|
||||
# somebody rolling out fifty instances from a template has answered those questions once already,
|
||||
# and making them answer each one in a browser is exactly what this product stopped doing to people.
|
||||
#
|
||||
# Set this and the instance installs itself at boot from whatever the variables above provide, marks
|
||||
# itself installed, and never shows the wizard.
|
||||
#
|
||||
#JARVIS_UNATTENDED=on
|
||||
#
|
||||
# The first administrator. REQUIRED when JARVIS_UNATTENDED is on and the database is empty; the
|
||||
# password must be at least 12 characters.
|
||||
#
|
||||
# If they are missing or too short the instance says so loudly in its logs and leaves setup
|
||||
# OUTSTANDING rather than completing — an instance marked installed with no account is one nobody
|
||||
# can ever get into, and the claim window is gated on setup being unfinished, so there would be no
|
||||
# way back. Falling through to the ordinary install screen is strictly better than that.
|
||||
#
|
||||
#JARVIS_ADMIN_EMAIL=
|
||||
#JARVIS_ADMIN_PASSWORD=
|
||||
#JARVIS_ADMIN_NAME=Administrator
|
||||
#
|
||||
# Worth setting alongside them, since nobody will be there to be asked: WEB_ORIGIN, OPENAI_API_KEY
|
||||
# and OPENAI_MODEL above, and JARVIS_LICENSE_KEY below. Each is optional even here — an unattended
|
||||
# instance with no model comes up and reports that it has none, rather than refusing to start.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Channels and pinning (optional, recommended in production)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -137,6 +202,22 @@ OPENAI_API_KEY=
|
||||
#AGENT_HEARTBEAT_INTERVAL_SEC=30
|
||||
#RUN_SHUTDOWN_GRACE_SEC=25
|
||||
|
||||
# How long a sign-in lasts, in seconds. Both were fixed in the compose file until now, which made
|
||||
# them the only tuning values on this page you could read about and not change.
|
||||
#
|
||||
# JWT_ACCESS_TTL is how long an access token stays valid — and therefore how long a revoked session
|
||||
# keeps working before it notices. Lower it if that window matters to you; the cost is a refresh
|
||||
# round trip more often.
|
||||
#
|
||||
# JWT_REFRESH_TTL IS AN IDLE TIMEOUT, NOT A SESSION LIFETIME, and the difference will matter to you
|
||||
# if you are here because of a policy. The refresh token rotates on every use and the new one starts
|
||||
# its full term from that moment, so nothing anywhere measures how old a sign-in is: somebody who
|
||||
# keeps a tab open stays signed in indefinitely. What this value really sets is how long a session
|
||||
# survives being left alone. Fourteen days is the default. To end sessions by age rather than by
|
||||
# idleness, revoke them — Settings → Security lists them and kills them individually.
|
||||
#JWT_ACCESS_TTL=900
|
||||
#JWT_REFRESH_TTL=1209600
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Licence (optional — without one you get the community edition)
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -167,4 +248,13 @@ OPENAI_API_KEY=
|
||||
#
|
||||
# A LICENSED instance reports regardless — that is part of what a licence is, and this variable does
|
||||
# not change it.
|
||||
# NOTE, 24 August 2026: this line did nothing before that date, and we are sorry. Compose reads
|
||||
# this file to fill in placeholders INSIDE the compose file — it does not hand it to the services —
|
||||
# and the api service never listed this variable, so the API saw nothing and applied its default of
|
||||
# "on". An instance that had opted out reported anyway. Pull the repository again and
|
||||
# `docker compose up -d`: the fix is in the compose file, not in the images.
|
||||
#
|
||||
# Do not trust this file to tell you whether it took. Ask the instance:
|
||||
# docker compose exec api printenv JARVIS_TELEMETRY
|
||||
# Settings -> Licence answers the same question from what the API actually resolved.
|
||||
#JARVIS_TELEMETRY=on
|
||||
|
||||
+313
@@ -9,6 +9,319 @@ one of them changed. An entry says which artefact and which version, so `web 0.1
|
||||
Every version published to a channel appears here. Nothing is written from memory afterwards: a build
|
||||
cannot be published without its entry.
|
||||
|
||||
## 2026-08-24 — Losing your database no longer puts your instance up for grabs `api 0.88.0` `web 0.111.0`
|
||||
|
||||
**If your database is ever lost or replaced, your instance was claimable by whoever loaded the page
|
||||
first.** From the outside it looked exactly like a brand-new deployment — no accounts, no completed
|
||||
setup — so the install screen opened its 30-minute window and offered a super-admin account. Except
|
||||
it was not a new deployment: it was yours, on a hostname already in DNS, already in people's browser
|
||||
history, and already dialled by every agent you have enrolled.
|
||||
|
||||
We found this the way these things get found. A `docker compose down -v` meant for a throwaway test
|
||||
stack landed on a production one, because the compose file that had been copied into that directory
|
||||
carried its own project name. The database went. What was left was an instance offering itself to the
|
||||
internet.
|
||||
|
||||
**What changes.** An instance that cannot prove it is new still opens the window — you need a way back
|
||||
in — but creating the first administrator now costs the last characters of your vault master key.
|
||||
You have it, in your `.env` or on the `jarvis_secrets` volume you were told to back up separately.
|
||||
Nobody else does.
|
||||
|
||||
**A genuine first installation is never asked for it**, and neither is a restart during a slow one.
|
||||
The test is the age of the vault key at the moment your database first saw it: seconds on a real
|
||||
first boot, months on a replacement. It is recorded once, so restarting the api to re-open a window
|
||||
does not change the answer.
|
||||
|
||||
**Nothing to do.** No new variable, no migration step. Instances that upgrade into this keep working
|
||||
exactly as they are — the rule only ever applies to an instance with an empty database, which yours
|
||||
is not.
|
||||
|
||||
## 2026-08-24 — A friendlier install, and a vault key you can only be handed once `api 0.87.0` `web 0.110.0`
|
||||
|
||||
**The install screen leads with Jarvis now.** The mark, the name and a welcome come before anything
|
||||
is asked of you — the first thing this product ever showed a new deployer used to be a password
|
||||
field with no explanation attached. Ten steps, about five minutes, and the road map on the first
|
||||
screen is honest about which ones you can skip.
|
||||
|
||||
**Only the address really needs you.** The model step used to refuse to continue without a model id
|
||||
_and_ a key, which trapped anybody evaluating Jarvis without a provider key in hand — on a product
|
||||
whose remote terminal, agent fleet and vault all work perfectly well without one. An untouched form
|
||||
now continues and saves nothing. A half-filled one still refuses: that is a mistake rather than a
|
||||
decision.
|
||||
|
||||
**The wizard now asks who may create an account.** Self-registration defaults to open, and nothing
|
||||
in the install had ever mentioned it — so an instance on a public hostname would accept an account
|
||||
from anybody who found it, the moment you finished. It is a switch on the address step, beside the
|
||||
field that decides who can reach the sign-in page at all. Change it later under **Settings →
|
||||
Platform → Sign-in**.
|
||||
|
||||
**The vault key is shown, and downloadable, exactly once.** The install screen hands you the file
|
||||
rather than telling you to go and run `docker compose exec` — that command is a wall for anybody
|
||||
deploying through Coolify, Portainer or a managed host, and what is behind the wall is the one value
|
||||
no backup can reconstruct. The screen says so before you click. Afterwards, and any time later, the
|
||||
key is on the host:
|
||||
|
||||
```sh
|
||||
docker compose exec api cat /var/lib/jarvis/secrets/vault-master-key
|
||||
```
|
||||
|
||||
### Fixed
|
||||
|
||||
- **An interrupted install could not be finished.** Coming back to the wizard after the organization
|
||||
step showed you an empty form and demanded a name, and the request behind it could only fail —
|
||||
the community edition allows one organization and you already had it. There was no way forward and
|
||||
no way around. It now recognises the organization you already have and moves on.
|
||||
- **A first boot with no configuration printed a red `ERROR` about `JARVIS_UNATTENDED`** on
|
||||
instances where it was not set. Cosmetic, but it was the first thing a new deployer read in
|
||||
`docker compose logs`.
|
||||
- **`JARVIS_ADMIN_EMAIL` and `JARVIS_ADMIN_PASSWORD` alone would install the instance silently**,
|
||||
with no `JARVIS_UNATTENDED=on`, and skip the wizard entirely. Those variables are what an
|
||||
unattended install needs _in addition to_ the flag, never a second way of asking for one. If you
|
||||
keep them in a shared shell or a fleet template, this is the release where they stop acting on
|
||||
their own.
|
||||
- **Installing in German or French left you in an English console** — the account the wizard creates
|
||||
now records the language you installed in.
|
||||
|
||||
### Changed
|
||||
|
||||
- `JWT_ACCESS_TTL` and `JWT_REFRESH_TTL` are read from `.env` like every other tuning value
|
||||
instead of being fixed in the compose file. `.env.example` documents what each actually does —
|
||||
the refresh one is an idle timeout, not the session lifetime its name suggests.
|
||||
- `PUBLIC_URL` is gone. It was a second variable for the same address as `WEB_ORIGIN` and had not
|
||||
been read by anything for two releases; it is the stored instance address now, on a settings
|
||||
screen. Remove it from your `.env` if it is still there — nothing breaks either way.
|
||||
|
||||
### New in the manual
|
||||
|
||||
The published README grew the procedures it was missing: restoring the vault key onto a new host
|
||||
(**do it before the first `up -d`** — a generated key is never overwritten, so bringing the stack up
|
||||
first makes yours unusable), getting back in when you are locked out of the only administrator
|
||||
account, moving an instance to a new address and what that does to enrolled agents, and which
|
||||
generated secrets can be rotated.
|
||||
|
||||
## 2026-08-24 — `JARVIS_TELEMETRY=off` did not work, and now does
|
||||
|
||||
**If you set `JARVIS_TELEMETRY=off` in `.env`, your instance has been reporting anyway.** We are
|
||||
sorry. The variable never reached the container: compose reads `.env` to fill in `${...}`
|
||||
placeholders inside the compose file, it does not hand that file to the services, and the `api`
|
||||
service never listed this one. So the API saw nothing, applied its documented default of "on", and
|
||||
checked in — while the README, the changelog and `.env.example` all told you that one line was
|
||||
enough.
|
||||
|
||||
What was sent is what the README has always listed and nothing more: your instance id and public
|
||||
key, the version and contract hash it runs, its public address, and counts of organizations, users,
|
||||
agents and assets. No names, no conversation content, nothing about the machines you administer.
|
||||
|
||||
**The fix is in the compose file, not in the application**, so pulling this repository again is the
|
||||
whole of it:
|
||||
|
||||
```sh
|
||||
git pull # or re-download docker-compose.yml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Then confirm it took, from the instance itself:
|
||||
|
||||
```sh
|
||||
docker compose exec api printenv JARVIS_TELEMETRY
|
||||
```
|
||||
|
||||
No output means the fix has not landed yet; `off` means you are silent. **Settings → Licence** says
|
||||
the same thing on screen, and that is the answer to trust — it reads what the API actually resolved
|
||||
rather than what a file claims.
|
||||
|
||||
If you would like the entries a silenced instance should never have created removed from our side,
|
||||
write to <antoine@luxit.be> and quote the instance id from that screen.
|
||||
|
||||
## web 0.109.0 — 2026-08-24
|
||||
|
||||
**The welcome now comes before the account.** The very first thing anybody saw of Jarvis was a form
|
||||
asking for a password — no name, no explanation, no idea what they were about to be given
|
||||
administrator rights over. The introduction needs no account, so it goes first, and creating the
|
||||
administrator is step two with a way back to it.
|
||||
|
||||
**Mail and the licence are two steps.** They shared a page and have nothing to do with each other —
|
||||
one is an app registration in your Entra tenant, the other a key from your supplier — which made one
|
||||
long screen out of two short questions. The licence step now leads with the thing most people need
|
||||
to hear: you almost certainly do not need a key, and the community edition is perpetual.
|
||||
|
||||
## web 0.108.0 — 2026-08-24
|
||||
|
||||
**The install opens on a welcome rather than on a checklist.** It used to begin with "Checks:
|
||||
database, Redis, vault key" — which tells somebody who has just deployed a product they have never
|
||||
run that they are already in the middle of something. The first screen now introduces Jarvis, says
|
||||
what the next steps will ask for, roughly how long it takes, and which of them you can skip.
|
||||
|
||||
**The checks moved to the end, and that is a correction rather than a preference.** Run first, they
|
||||
had nothing to look at: your public address had not been chosen, so there was no URL to probe and no
|
||||
hostname to judge security keys against — three of the six findings were about a deployment that did
|
||||
not exist yet. They now run immediately before the screen that commits the install, against the one
|
||||
you have just described.
|
||||
|
||||
**Reporting has a step of its own.** It shared a page with mail and the licence, where a question
|
||||
about what leaves your network sat underneath two forms. It is the only decision in the wizard we
|
||||
benefit from, which is exactly why it gets a page you cannot scroll past.
|
||||
|
||||
**The step markers no longer collide.** Eight labels never fit the wizard column at any screen width
|
||||
and ran into each other. The strip is markers now — filled where you are, ticked behind you — with
|
||||
the step named in full underneath and the whole road laid out on the welcome screen.
|
||||
|
||||
## api 0.86.0 · web 0.107.0 — 2026-08-24
|
||||
|
||||
**Mail and the licence are configured IN the wizard now, not somewhere else.** That step used to
|
||||
show two paragraphs whose only affordance was a link to the settings screens — and those links could
|
||||
not work: an instance that has not finished installing sends every other address back to the install
|
||||
screen, so clicking one opened a tab that bounced straight back. The real forms are on the step now,
|
||||
the same ones you meet under Settings afterwards, and nothing on it is required.
|
||||
|
||||
**You can decide there whether this instance reports to us.** Reporting used to be an environment
|
||||
variable and nothing else, so a fresh install had no way to answer the question without editing a
|
||||
compose file. It is a switch on that step and on Settings → Licence.
|
||||
|
||||
`JARVIS_TELEMETRY=off` keeps its authority: an instance whose host has switched reporting off shows
|
||||
the control locked and says why. Deciding what leaves your network stays with whoever runs the host,
|
||||
which is exactly why it was a variable in the first place — the switch adds the case where the person
|
||||
installing the instance IS that person, and the case where you want to fall silent later without a
|
||||
restart.
|
||||
|
||||
**Your vault key is a download.** The last step used to print `docker compose exec api cat …` and ask
|
||||
you to go and run it. If you deployed through Coolify, Portainer or a managed host, that was a wall
|
||||
in front of the one thing here that no backup can reconstruct. Now the key is shown on the page with
|
||||
a button that saves it as a file, and the command is still underneath for anyone who would rather
|
||||
take it off the host.
|
||||
|
||||
That view exists only while the install is unfinished. Once you press Finish it is refused for good,
|
||||
and every time it was used is in the audit trail.
|
||||
|
||||
**Unattended installation, for fleets and for CI.** Set `JARVIS_UNATTENDED=on` with
|
||||
`JARVIS_ADMIN_EMAIL` and `JARVIS_ADMIN_PASSWORD` and the instance installs itself at boot from the
|
||||
variables you already provide, and never shows the wizard. Everything else stays optional.
|
||||
|
||||
If those credentials are missing or the password is under twelve characters it says so loudly and
|
||||
leaves the install UNFINISHED rather than completing. An instance marked installed with no account
|
||||
is one nobody can ever get into, and no restart recovers it — falling through to the ordinary install
|
||||
screen is strictly better.
|
||||
|
||||
## web 0.106.3 — 2026-08-24
|
||||
|
||||
**The install screen tells you where you are.** Its step list was six labels in a row where the
|
||||
current one differed by a font weight — and the difference did not even arrive, because the class
|
||||
meant to dim the others named a colour this console does not define. Every label rendered
|
||||
identically, so the one screen whose whole job is to walk you through six steps could not say which
|
||||
of them you were on.
|
||||
|
||||
The stepper now carries that three ways at once, none of them colour alone: a tick for what is done
|
||||
and a filled number for where you are, connectors that fill in behind you, and a line that names the
|
||||
step — "Step 3 of 6 · The model" — which is also what makes it readable on a phone, where the labels
|
||||
step aside.
|
||||
|
||||
The same missing colour had flattened the rest of the wizard: hints and explanations rendered as
|
||||
bright as the headings above them, error messages were not red, and the boxes around the warnings
|
||||
had no edge. All of it reads properly now.
|
||||
|
||||
## api 0.85.3 — 2026-08-24
|
||||
|
||||
**An instance still being installed no longer reports itself.** It used to check in thirty seconds
|
||||
after boot, before anybody had chosen its address — so a fresh install announced itself as
|
||||
`http://localhost:3000`, with every count at zero. That is worse than silence: the address exists in
|
||||
the report so that your provider can tell one installation from another, and a list of identical
|
||||
localhost entries answers nothing.
|
||||
|
||||
It now waits until the install screen is finished, then reports normally with the address you chose.
|
||||
Nothing is lost, only deferred. `JARVIS_TELEMETRY=off` still silences it entirely.
|
||||
|
||||
## web 0.106.2 — 2026-08-23
|
||||
|
||||
**A language picker on the install screen.** Every other page that can be reached without an account
|
||||
has one — the sign-in page, a shared transcript — and the install wizard, which is the longest piece
|
||||
of reading this product puts in front of somebody who has not yet decided to trust it, did not.
|
||||
|
||||
It follows your browser as before; the picker is for the case the browser is wrong, which is
|
||||
routinely: installing from a colleague's laptop, or on a server whose locale nobody set. Before the
|
||||
first account exists the choice is remembered in this browser; after it, it is saved to your account
|
||||
as well — so it also corrects the language the claim guessed.
|
||||
|
||||
## api 0.85.2 · web 0.106.1 — 2026-08-23
|
||||
|
||||
**Installing in German now ends in a German console.** The install screen has always followed your
|
||||
browser, in English, French or German — but the account it created took the database default of
|
||||
English, and the console adopted that the moment the wizard handed over. Somebody who had just read
|
||||
seven screens in their own language was greeted in another and left to find the language switcher.
|
||||
|
||||
The account now records the language the wizard was read in. Change it whenever you like under
|
||||
Settings → Profile; this only decides where you start.
|
||||
|
||||
## api 0.85.1 — 2026-08-23
|
||||
|
||||
**A fresh install no longer stores an address nobody chose.** api 0.85.0 wrote
|
||||
`http://localhost:3000` as the public address of any instance that started with no configuration —
|
||||
the schema default for `WEB_ORIGIN`, saved as though somebody had decided it. The boot banner then
|
||||
told an operator on a remote host to open a URL that leads nowhere.
|
||||
|
||||
The address is now left unset until you choose it, which is what the install screen already assumed:
|
||||
it fills the field from the address you are reading it at, and the checks say plainly that nothing is
|
||||
set yet. The banner names the path and not the host — you know how you reach the machine, you were
|
||||
only missing `/install`.
|
||||
|
||||
Nothing to do if you set `WEB_ORIGIN` yourself: it is still read, once, exactly as before.
|
||||
|
||||
## api 0.85.0 · web 0.106.0 — 2026-08-23
|
||||
|
||||
**Installing Jarvis no longer starts with a text editor.**
|
||||
|
||||
```sh
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Then open it in a browser. An install screen creates the first administrator, checks the deployment
|
||||
and asks for the rest. **The `.env` step is gone** — it demanded six values before the stack would
|
||||
start, four of them secrets you had to generate with `openssl`, and the whole file is optional now.
|
||||
|
||||
**A wrong model key is a message instead of a crash loop.** This is the change underneath all the
|
||||
others. The api validated its configuration at boot and called `process.exit(1)` when anything was
|
||||
missing — so a mistyped key, an account out of credit or an endpoint that had moved presented as a
|
||||
container that would not stay up, with the actual reason on line 40 of a log you had no reason to
|
||||
open. An unconfigured instance now starts, says what it needs, and the model screen has a **test
|
||||
button that asks the model for a token before saving**, showing the provider's own words when it
|
||||
refuses.
|
||||
|
||||
**A brand-new instance can be claimed for 30 minutes, and this closes a real hole.** Jarvis used to
|
||||
accept exactly one registration on an instance with no users and make that account super-admin — so
|
||||
a freshly deployed instance reachable from the internet belonged to whoever found it first, on a
|
||||
product that holds SSH keys and opens shells on your clients' servers. The install screen shows a
|
||||
countdown; `docker compose restart api` opens another window if you miss it.
|
||||
|
||||
**Everything the wizard asks has a permanent screen.** Settings → Platform gained two tabs: **Model**
|
||||
— endpoint, key, model and thinking level, with the same test button the wizard uses — and
|
||||
**Address**, for your public address and proxy-hop count. The wizard is a first-run convenience, not
|
||||
the only way in: an instance whose OpenAI key has been rotated, or whose hostname has moved, says so
|
||||
on a settings page rather than being re-installed.
|
||||
|
||||
**The proxy setting is now something you are shown rather than asked to guess.** `TRUST_PROXY_HOPS`
|
||||
decided which address landed in your audit trail, and nobody can know it in advance. The install
|
||||
screen displays the `X-Forwarded-For` chain your own request actually carried and the address the
|
||||
API resolved from it, and you confirm what you see.
|
||||
|
||||
**Your secrets are generated for you, into a new `jarvis_secrets` volume.** Back it up, **separately
|
||||
from the database** — the vault key lives there and nowhere else, and a database backup holds only
|
||||
ciphertext. The install screen will not finish until you have read the key and typed its last
|
||||
characters back. The key never leaves the server: there is deliberately no endpoint that returns it.
|
||||
|
||||
### Upgrading from an earlier build? There is nothing to do
|
||||
|
||||
Leave your `.env` exactly as it is. Your secrets are **adopted** into the new volume rather than
|
||||
replaced, so the vault is untouched. `WEB_ORIGIN` becomes the stored public address, your `OPENAI_*`
|
||||
values become the stored model settings, and an instance that already has an administrator is marked
|
||||
installed by a backfill — **you will never see the install screen**. Everything those variables used
|
||||
to govern is now a settings page; the variables still seed a fresh install and are otherwise ignored.
|
||||
|
||||
One thing did change for you: **`PUBLIC_URL` is no longer read.** It was a second variable for the
|
||||
same address. There is one now, seeded from `WEB_ORIGIN` and edited under Settings → Platform.
|
||||
|
||||
**Changing your public address orphans enrolled passkeys.** It always did — a passkey is bound to the
|
||||
hostname it was created under — but it was previously a variable nobody edited twice. Now that it is
|
||||
a field on a screen, the screen warns you before it saves.
|
||||
|
||||
## api 0.84.0 · web 0.105.0 — 2026-08-21
|
||||
|
||||
**Ask for a licence from inside Jarvis.** Settings → Licence has a form: your company, somebody to
|
||||
|
||||
@@ -165,7 +165,8 @@ inventory on every heartbeat.
|
||||
builds for Linux, macOS and Windows.
|
||||
- A hostname and a TLS terminator in front of it. Jarvis speaks plain HTTP and does not manage
|
||||
certificates.
|
||||
- An API key for an OpenAI-compatible endpoint. The api will not start without one.
|
||||
- An API key for an OpenAI-compatible endpoint. The install screen asks for it and tests it before
|
||||
saving; you do not need it in hand before you start.
|
||||
- **No licence key.** Jarvis runs the [community edition](#the-community-edition) out of the box; a
|
||||
key raises the limits when you outgrow them.
|
||||
- Roughly 2 GB of RAM for the stack and room for Postgres to grow.
|
||||
@@ -173,25 +174,78 @@ inventory on every heartbeat.
|
||||
## Install
|
||||
|
||||
```sh
|
||||
git clone https://git.luxit.be/Luxit/jarvis-selfhost.git
|
||||
cd jarvis-selfhost
|
||||
cp .env.example .env
|
||||
$EDITOR .env # every comment in it is load-bearing; the secrets note especially
|
||||
docker compose pull
|
||||
curl -O https://git.luxit.be/Luxit/jarvis-selfhost/raw/branch/main/docker-compose.yml
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
First boot syncs the database schema and runs its data backfills before the API listens, so give it
|
||||
about a minute. Then point your reverse proxy at `JARVIS_PORT` and open the app.
|
||||
That is the whole first deployment. **There is no `.env` step.** First boot generates the stack's
|
||||
secrets, syncs the database schema and runs its data backfills before the API listens, so give it
|
||||
about a minute. Then point your reverse proxy at port 8080 and open the address in a browser.
|
||||
|
||||
**Give Jarvis its public address with the right scheme.** `WEB_ORIGIN` is the CORS decision and it
|
||||
accepts exactly one origin; `PUBLIC_URL` is what gets baked into agent install commands, invitation
|
||||
links and share links. A wrong `WEB_ORIGIN` presents as a sign-in page that cannot sign in. A wrong
|
||||
`PUBLIC_URL` does not fail at deploy time — it fails weeks later, on somebody else's server.
|
||||
An install screen takes it from there. It welcomes you, creates the first administrator inside a
|
||||
bounded window, then asks for the address this instance answers on and who may create an account on
|
||||
it, the model, your first organization, and — every one of them skippable — mail, a licence and
|
||||
whether this instance reports anything about itself. It finishes by showing you what the deployment
|
||||
actually looks like from inside, and by making you take a backup of the one value nothing can
|
||||
reconstruct. Ten screens, about five minutes, and **only the address really needs you**: everything
|
||||
else has a "later" that costs nothing.
|
||||
|
||||
**The first account created becomes super-admin.** Self-registration is open by default, so sign up
|
||||
immediately after the stack is up and then close registration under **Settings → Platform →
|
||||
General**. Leaving it open means anyone who reaches the sign-in page gets an account.
|
||||
Everything it asks is a setting you can change afterwards from an ordinary screen. Nothing it asks
|
||||
needs a container restart to change.
|
||||
|
||||
### The claim window
|
||||
|
||||
**A brand-new instance can be claimed for 30 minutes after it starts, and the first account created
|
||||
owns it.** That is why the install screen shows a countdown.
|
||||
|
||||
This replaces something worse. Jarvis used to accept exactly one registration on an instance with no
|
||||
users and make that account super-admin — so a freshly deployed instance reachable from the internet
|
||||
belonged to whoever found it first, on a product that holds SSH keys and opens shells on your
|
||||
clients' servers. The window closes that.
|
||||
|
||||
If you miss it, nothing is lost. Restart the api container to open another:
|
||||
|
||||
```sh
|
||||
docker compose restart api
|
||||
```
|
||||
|
||||
**Losing your database does not put your instance up for grabs.** An instance whose database has gone
|
||||
missing looks exactly like a brand-new one from the outside — no accounts, no completed setup — while
|
||||
sitting on a hostname the world already knows. On that instance the window still opens, so you can
|
||||
get back in, but creating the first administrator costs the last characters of your vault master key.
|
||||
You have it; nobody else does. A genuine first installation is never asked for it.
|
||||
|
||||
### The one thing to do afterwards
|
||||
|
||||
**Back up the `jarvis_secrets` volume, and not to the same place as your database.**
|
||||
|
||||
It holds this instance's vault master key, which is generated on first boot and exists nowhere else.
|
||||
Every credential in the vault, the licence identity, every authenticator secret and every terminal
|
||||
recording is encrypted under it. **A database backup does not save you** — the backup holds the
|
||||
ciphertext. An instance whose key is gone keeps looking configured, with every row in place, and
|
||||
fails on the first reveal.
|
||||
|
||||
The install screen shows you the key, offers it as a file to download, and will not let you finish
|
||||
until you have typed its last characters back. **Take the download while it is on screen** — that is
|
||||
the easiest moment this value will ever be available to you.
|
||||
|
||||
Afterwards, and any time later, read it from the host:
|
||||
|
||||
```sh
|
||||
docker compose exec api cat /var/lib/jarvis/secrets/vault-master-key
|
||||
```
|
||||
|
||||
### If you prefer to configure it in writing
|
||||
|
||||
`.env.example` is still there and every value in it is optional — image pinning, the host port,
|
||||
the agent overlay. The ones that overlap with the install screen SEED it: they are read once, when
|
||||
nothing is stored yet, and never again. Change them afterwards and nothing happens; change the
|
||||
setting instead.
|
||||
|
||||
**Upgrading from a stack that already has a `.env`?** Leave it exactly as it is. Your secrets are
|
||||
adopted into the volume rather than replaced, your `WEB_ORIGIN` becomes the stored public address,
|
||||
your model settings are adopted the same way, and an instance that already has an administrator is
|
||||
marked installed by a backfill — so you will never see the install screen. Nothing to do.
|
||||
|
||||
## Your reverse proxy has two requirements
|
||||
|
||||
@@ -206,9 +260,12 @@ Both are the kind that produce confusing symptoms rather than clean errors.
|
||||
a byte, and a 60s default cuts the response mid-stream. The client sees a connection reset rather
|
||||
than a timeout, which reads like a bug in Jarvis.
|
||||
|
||||
Forward `X-Forwarded-For` too, and set `TRUST_PROXY_HOPS` to match how many proxies rewrite it —
|
||||
that is what puts real client addresses in the audit trail and the session list instead of your
|
||||
proxy's.
|
||||
Forward `X-Forwarded-For` too. How many proxies rewrite it is a setting, and the install screen is
|
||||
where you answer it — **it shows you the chain your own request actually carried and the address the
|
||||
API resolved from it**, so you confirm what you see rather than counting hops from memory. That is
|
||||
what puts real client addresses in the audit trail and the session list instead of your proxy's, and
|
||||
setting it too high is the dangerous direction: the API would then believe that many hops of a
|
||||
header a caller can forge. Change it later under **Settings → Platform**.
|
||||
|
||||
## Upgrading
|
||||
|
||||
@@ -322,10 +379,19 @@ the grant. Decide who holds those before enrolling anything you care about.
|
||||
|
||||
These are deliberate and documented rather than surprises waiting to be found.
|
||||
|
||||
- **`VAULT_MASTER_KEY` has no recovery.** Read the note in `.env.example`. A database backup does not
|
||||
protect what it seals — the backup holds ciphertext encrypted under that key, and that covers more
|
||||
than the vault: the outbound-mail client secret, this instance's licence identity key, every TOTP
|
||||
secret and every terminal recording go with it.
|
||||
- **The vault key has no recovery.** It is generated on first boot into the `jarvis_secrets` volume
|
||||
and exists nowhere else. A database backup does not protect what it seals — the backup holds
|
||||
ciphertext encrypted under that key, and that covers more than the vault: the outbound-mail client
|
||||
secret, this instance's licence identity key, every TOTP secret and every terminal recording go
|
||||
with it. The install screen makes you read it and type its last characters back before it will
|
||||
finish, which is the only reason anybody would. See [Backups](#backups-and-restoring-one).
|
||||
**It also cannot be rotated.** There is no procedure that re-wraps existing data under a new key,
|
||||
so if this value is disclosed — read out on a screen share, pasted into a ticket, on a laptop that
|
||||
walked — the answer is a new instance and a fresh set of credentials, not a rotation. The other
|
||||
three generated secrets are ordinary: delete the file from `jarvis_secrets` and restart, and the
|
||||
init service writes a new one. Doing that to `jwt-access-secret` or `jwt-refresh-secret` signs
|
||||
everybody out, which is usually the point; doing it to `postgres-password` needs the database's own
|
||||
password changed to match, so plan that one.
|
||||
- **SSH host keys are not verified.** Every SSH connection trusts whatever key answers. This is the
|
||||
one gap in the execution path with no compensating control.
|
||||
- **The api must run as a single replica.** In-flight runs, pending approvals, presence and the
|
||||
@@ -365,14 +431,18 @@ docker compose ps # who is running, and who is restarting
|
||||
docker compose logs -f api # the api says why it refused to start
|
||||
```
|
||||
|
||||
The api validates its configuration at boot and **fails fast rather than starting half-configured**,
|
||||
so a first-boot failure is almost always one line of `.env`:
|
||||
**An unconfigured instance now starts and says so** rather than refusing to boot. That is the
|
||||
change: a wrong model key used to make the api exit 1, and the restart policy turned it into a crash
|
||||
loop with the reason buried in a log. It comes up, shows the install screen, and tells you what is
|
||||
wrong on the screen that asks for it.
|
||||
|
||||
| Symptom | Cause |
|
||||
| --- | --- |
|
||||
| `docker compose up` exits complaining about a variable | That variable is empty in `.env`. Compose refuses before any container starts. |
|
||||
| api restarts in a loop, logs `Invalid environment configuration` | A value is present but wrong. `VAULT_MASTER_KEY` must base64-decode to exactly 32 bytes. |
|
||||
| The sign-in page loads but cannot sign in | `WEB_ORIGIN` is not the address the browser used, scheme included. It accepts one origin, not a list. |
|
||||
| Every page redirects to an install screen | This instance has not been installed yet. That is the wizard, not an error. |
|
||||
| The install screen says the claim window has closed | Nobody claimed it within 30 minutes of boot. `docker compose restart api` opens another. |
|
||||
| The assistant answers "this instance has no model configured" | Exactly that. **Settings → Platform → Model**, where the test button will tell you what the provider thinks. |
|
||||
| api restarts in a loop, logs `Invalid environment configuration` | A value you set yourself is wrong. Only the database, Redis and the vault key are validated at boot now. |
|
||||
| The sign-in page loads but cannot sign in | The stored public address is not the one the browser used, scheme included. Change it under **Settings → Platform**. |
|
||||
| Chat never answers and nothing streams | The WebSocket upgrade is not being forwarded. |
|
||||
| An answer dies part-way through, every time | The proxy's read timeout is too short. |
|
||||
| Everything works but one kind of thing cannot be created | A limit is full — the community edition's, or your licence's. The message names which. See below. |
|
||||
@@ -381,20 +451,110 @@ so a first-boot failure is almost always one line of `.env`:
|
||||
The api takes about a minute on first boot, syncing the schema before it listens. `docker compose ps`
|
||||
showing `health: starting` for that long is expected, not a fault.
|
||||
|
||||
### When you are locked out of the only administrator account
|
||||
|
||||
There is no password reset in this product — no email flow, no "forgot password" link. That is
|
||||
deliberate, and it means the single super-admin the wizard creates is a single point of failure until
|
||||
you do something about it.
|
||||
|
||||
**Do this now, not later: make a second super-admin.** **Settings → Users → New user**, platform role
|
||||
*Super admin*, and set their password on the same screen. Outbound mail is not required for it. Two
|
||||
minutes, and it turns every case below into somebody else clicking a button.
|
||||
|
||||
**If another account can still sign in**, promote it and let it fix the first:
|
||||
|
||||
```sh
|
||||
docker compose exec -T postgres psql -U jarvis -d jarvis \
|
||||
-c "UPDATE \"User\" SET \"platformRole\" = 'SUPER_ADMIN' WHERE email = 'colleague@example.com';"
|
||||
```
|
||||
|
||||
They then reset the locked-out password under **Settings → Users**.
|
||||
|
||||
**If no account can sign in at all**, set a password hash directly. Jarvis stores argon2id, and the
|
||||
api image carries the library that makes one — so the hash is generated by the same code that will
|
||||
check it:
|
||||
|
||||
```sh
|
||||
docker compose exec api node -e \
|
||||
"const a=require('argon2');a.hash(process.argv[1],{type:a.argon2id}).then(h=>console.log(h))" \
|
||||
'the-new-password-at-least-12-characters'
|
||||
```
|
||||
|
||||
Then write it, and make sure the account is active and privileged:
|
||||
|
||||
```sh
|
||||
docker compose exec -T postgres psql -U jarvis -d jarvis -c \
|
||||
"UPDATE \"User\" SET \"passwordHash\" = '<the \$argon2id\$… string>', \"isActive\" = true,
|
||||
\"platformRole\" = 'SUPER_ADMIN' WHERE email = 'you@example.com';"
|
||||
```
|
||||
|
||||
Quote the hash in single quotes — it contains `$` characters your shell would otherwise eat.
|
||||
|
||||
If the account also holds a second factor you no longer have: when another administrator exists, they
|
||||
do it properly from **Settings → Users**, which strips the factors, ends every open session and files
|
||||
an audit entry. With nobody left to click it, clear all three parts by hand — the passkeys, the
|
||||
authenticator secret, and the flag that says a factor is expected — or the account will keep
|
||||
demanding one:
|
||||
|
||||
```sh
|
||||
docker compose exec -T postgres psql -U jarvis -d jarvis <<'SQL'
|
||||
DELETE FROM "WebAuthnCredential" WHERE "userId" = (SELECT id FROM "User" WHERE email = 'you@example.com');
|
||||
DELETE FROM "UserTotpCredential" WHERE "userId" = (SELECT id FROM "User" WHERE email = 'you@example.com');
|
||||
UPDATE "User" SET "mfaEnabled" = false, "mfaEnabledAt" = NULL WHERE email = 'you@example.com';
|
||||
SQL
|
||||
```
|
||||
|
||||
Enrol a new factor as soon as you are back in, and note that this leaves no audit trail of its own —
|
||||
the trail is your shell history.
|
||||
|
||||
**Re-running the install wizard is not a recovery route.** Its claim window only opens on an instance
|
||||
with no users and no completed setup, so on a working deployment it will refuse — and making it
|
||||
refuse less by clearing those columns by hand would hand your live instance to whoever reaches it
|
||||
first.
|
||||
|
||||
### Moving the instance to a new address
|
||||
|
||||
Changing the public address under **Settings → Platform → Instance** is supported, and it has two
|
||||
consequences that are not obvious and not reversible by changing it back.
|
||||
|
||||
**Enrolled agents keep dialling the old one.** Each machine stores the address it was enrolled with
|
||||
and does not learn a new one from the server. After a move they go OFFLINE and stay there. Every
|
||||
agent has to be re-enrolled against the new address, so a move is a job scheduled with whoever
|
||||
administers those machines rather than a settings change made on a Friday.
|
||||
|
||||
**Every passkey stops being offered.** A passkey is bound to the hostname it was enrolled under. They
|
||||
do not fail loudly — the browser simply stops presenting them — so accounts quietly fall back to
|
||||
passwords, and anyone who set up a passkey and never learned their password is locked out. Make sure
|
||||
the people who use passkeys know their passwords, or enrol them again afterwards, before you move.
|
||||
|
||||
Nothing else moves: conversations, credentials, assets and recordings are rows and do not know the
|
||||
address. Update your reverse proxy and the `WEB_ORIGIN`-shaped world outside first, then the setting,
|
||||
then the agents.
|
||||
|
||||
## Backups, and restoring one
|
||||
|
||||
Postgres holds everything except the vault key — avatars, documents, exports and terminal recordings
|
||||
are all rows, not files on disk. **Of the stack's volumes, only `postgres_data` is worth backing
|
||||
up.** `redis_data` carries nonces, rate-limit counters and the socket fan-out, all of which rebuild
|
||||
themselves; `agent_releases` is refilled by the next `docker compose pull`.
|
||||
are all rows, not files on disk. **Two volumes are worth backing up, and they must go to different
|
||||
places:** `postgres_data` and `jarvis_secrets`. `redis_data` carries nonces, rate-limit counters and the
|
||||
socket fan-out, all of which rebuild themselves; `agent_releases` is refilled by the next
|
||||
`docker compose pull`.
|
||||
|
||||
They are separate volumes precisely so that they can be, and must be, backed up separately. A dump
|
||||
that travelled with the key that opens it is a dump that opens itself.
|
||||
|
||||
```sh
|
||||
docker compose exec -T postgres pg_dump -U jarvis jarvis | gzip > jarvis.sql.gz
|
||||
```
|
||||
|
||||
Plus `VAULT_MASTER_KEY`, stored somewhere that is not this host. A dump without the key is a database
|
||||
whose credentials cannot be read — and an instance restored under a *different* key keeps **looking**
|
||||
configured, because nothing on a settings page decrypts anything. It fails on every reveal instead.
|
||||
Plus the vault key, stored somewhere that is not this host and not beside the dump:
|
||||
|
||||
```sh
|
||||
docker compose exec api cat /var/lib/jarvis/secrets/vault-master-key
|
||||
```
|
||||
|
||||
A dump without the key is a database whose credentials cannot be read — and an instance restored
|
||||
under a *different* key keeps **looking** configured, because nothing on a settings page decrypts
|
||||
anything. It fails on every reveal instead.
|
||||
|
||||
To restore, stop the api so nothing writes while you work, then load the dump into an empty database:
|
||||
|
||||
@@ -409,6 +569,40 @@ docker compose start api web
|
||||
Restore under the **same `VAULT_MASTER_KEY`** the dump was taken with. That value is not in the dump,
|
||||
and no part of the restore will warn you that it differs.
|
||||
|
||||
### Putting the key back on a new host
|
||||
|
||||
**Do this before the first `docker compose up -d`, not after.** The generator writes each secret with
|
||||
`O_EXCL` and never overwrites one that exists, which is what stops an upgrade quietly replacing the
|
||||
key your vault is sealed under. On a fresh host it cuts the other way: bring the stack up first and
|
||||
a brand-new key is written, after which anything you put in `.env` is ignored for good.
|
||||
|
||||
If you kept the `jarvis_secrets` volume, restore it and nothing else is needed. If all you have is
|
||||
the base64 string, put it in `.env` before the first start:
|
||||
|
||||
```sh
|
||||
# On the NEW host, in the directory holding docker-compose.yml — before any `up -d`.
|
||||
echo 'VAULT_MASTER_KEY=<the base64 string you saved>' >> .env
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The `init` service adopts that value into the volume on the first run and the api reads it from
|
||||
there afterwards, so the line in `.env` is a seed rather than a permanent setting — you may remove it
|
||||
once the stack is up.
|
||||
|
||||
If you have already started the stack and a wrong key was generated, delete the file and let the init
|
||||
service run again. **Only ever do this on a host whose vault you are deliberately re-keying** — on a
|
||||
working instance it destroys every credential, the mail client secret, this instance's licence
|
||||
identity, every TOTP secret and every terminal recording:
|
||||
|
||||
```sh
|
||||
docker compose down
|
||||
docker run --rm -v jarvis_secrets:/s alpine rm -f /s/vault-master-key
|
||||
# then the `.env` line above, then `docker compose up -d`
|
||||
```
|
||||
|
||||
To check you restored under the right one, reveal a stored credential in **Settings → Vault**. The
|
||||
rows are all there under a wrong key; only a reveal tells you the truth.
|
||||
|
||||
## Removing it
|
||||
|
||||
```sh
|
||||
@@ -460,6 +654,21 @@ reach us would just be an email with extra steps.
|
||||
feature and every machine; what you lose is the request form, and our ability to know you exist. That
|
||||
is a fair trade and we would rather you took it than felt watched.
|
||||
|
||||
> **This did not work before 24 August 2026, and we are sorry.** The line was read from `.env` by
|
||||
> compose — which uses that file to fill in placeholders inside the compose file, not to hand values
|
||||
> to the services — and the `api` service never listed the variable, so the API saw nothing and
|
||||
> applied its default of "on". An instance that had opted out reported anyway. Pull this repository
|
||||
> again and `docker compose up -d`; the fix is in the compose file, not in the images.
|
||||
>
|
||||
> **Do not trust a file to tell you.** Ask the instance:
|
||||
>
|
||||
> ```sh
|
||||
> docker compose exec api printenv JARVIS_TELEMETRY
|
||||
> ```
|
||||
>
|
||||
> No output means it still has not landed. **Settings → Licence** answers the same question on
|
||||
> screen, from what the API actually resolved — which is why that screen exists.
|
||||
|
||||
Everything in the next section applies to a community instance as well: the field list is the whole
|
||||
field list, and it is the same one.
|
||||
|
||||
|
||||
+117
-19
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user