Self-hosting Jarvis
Jarvis is an AI-assisted infrastructure administration platform for MSPs. This repository runs it from published container images — no source, no build, no account with the project.
What you need
- Docker with Compose v2, on anything Linux.
- A hostname and a TLS terminator in front of it. Jarvis speaks plain HTTP and reads
X-Forwarded-Proto; it does not manage certificates. - An API key for an OpenAI-compatible endpoint.
- Roughly 2 GB of RAM for the stack and room for Postgres to grow.
Install
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
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.
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.
Your reverse proxy has two requirements
Both are the kind that produce confusing symptoms rather than clean errors.
- Forward the WebSocket upgrade. Without it the chat cannot stream and no agent can connect.
- Give it a long read timeout — 300s or so. A reasoning model can go 90+ seconds without emitting 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.
Upgrading
docker compose pull && docker compose up -d
That is the whole upgrade: both services track latest. Schema changes apply themselves when the api
starts, and the api and the web are versioned independently — their numbers are not meant to match,
because usually only one side changed.
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 when it is built — so the footer in the
app and /version.json keep reporting the real number, whichever name you pulled it under. That is
what lets you tell somebody which build you are on when something goes wrong.
Once you are in production, consider pinning: set JARVIS_IMAGE_API and JARVIS_IMAGE_WEB in .env
to explicit tags. It makes an upgrade a decision rather than a side effect of pulling.
Take a database dump before an upgrade that moves the api's minor version:
docker compose exec -T postgres pg_dump -U jarvis jarvis | gzip > jarvis-$(date +%F).sql.gz
What you get, and what you do not
Working: the assistant with its tool-calling loop, plan-level approvals for destructive operations, the encrypted vault, the asset registry, multi-tenant RBAC, real-time conversations, documents and exports, the audit trail, and the connectors — SSH, Proxmox, Microsoft 365 and MikroTik.
The Jarvis agent is an optional overlay, off by default. Enrolling a machine downloads a compiled
binary that the api serves from AGENT_RELEASE_DIR, and a compose-only deployment has no way to
produce one. docker-compose.agent.yml supplies it as a pullable image instead. Leaving it off is a
supported state rather than a broken one: everything else works, the installer answers 503 saying no
build is published, and the connectors above reach machines without it.
The agent
Turn it on by adding one line to .env, so that every later docker compose command picks up both
files with no extra flags:
COMPOSE_FILE=docker-compose.yml:docker-compose.agent.yml
then docker compose pull && docker compose up -d. A one-shot agent-releases service copies the
release into a volume the api reads, and exits. From there, Settings → Agents hands you the install
one-liner for each platform.
Three things worth knowing about it:
- Until that publisher exits cleanly, the api does not start. That is deliberate — a release that failed to arrive should stop the deploy loudly rather than leave you handing 404s to every installer you run this week. The cost is that an unreachable registry blocks the whole stack. The comment in the file names the three lines to drop if you would rather it degraded quietly.
- Upgrading it does not restart anything. The api computes each download's checksum from the bytes on disk on every request, so a new release in the volume is served immediately.
- The agent version is its own number. It moves independently of the api and the web, and a Jarvis
release usually does not touch it at all. Pin it with
JARVIS_IMAGE_AGENTwhen you pin the others.
The agent runs as root on Linux and macOS and as LocalSystem on Windows, deliberately — its purpose is to administer the machine. What it may actually do is decided by Jarvis' autonomy policy and its approval gates, not by the account it runs under. Read that section of the main documentation before enrolling anything you care about.
Things worth knowing before you trust it with production
These are deliberate and documented rather than surprises waiting to be found.
VAULT_MASTER_KEYhas no recovery. Read the note in.env.example. A database backup does not protect the vault — the backup holds ciphertext encrypted under that key.- 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.
- Access tokens cannot be revoked. Revoking a session or changing a password invalidates refresh tokens; a stolen access token stays valid for up to 15 minutes.
- The api must run as a single replica. In-flight runs, pending approvals and presence live in per-process memory. Scaling it out silently drops cancels and approvals answered on the wrong one.
- Both containers run as root, and
/api/healthanswers 200 withstatus: "degraded"when the database is unreachable, so the container healthcheck alone is not a liveness signal for the DB. - The assistant executes real operations on real infrastructure. Destructive ones require in-chat human approval; mutating ones do not. Decide your autonomy level per conversation accordingly.
Backups
Postgres holds everything except the vault key. Two volumes matter:
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.
Licence keys, and what your instance reports
Jarvis needs a licence key. Ask your provider for one and put it in .env as
JARVIS_LICENSE_KEY. The key is a signed token your instance verifies offline — it carries your
term and your limits, and it needs no network to be checked.
Without one, an instance keeps running everything already set up — every organization, every user, every agent, and the assistant itself — and refuses to create anything NEW. So an existing deployment does not stop working when this reaches it, and a fresh install gets as far as its first administrator account and then needs a key.
An unlicensed instance contacts nobody. No check-in, no telemetry, nothing leaves your network at all. The reporting below starts only once a key is in place.
A licensed instance then reports to the address written into that key, at an interval your provider sets — every ten minutes on the current arrangement, so that a renewal or a revocation reaches you promptly rather than tomorrow. This is everything it sends, in full:
| Field | What it is |
|---|---|
| Product | The literal string jarvis |
| Licence id | Which licence this is |
| Instance id + public key | A key pair your instance generated, identifying it |
| Version | Which Jarvis build you are running |
| Contract version | Which set of limits this build understands — a checksum, not a document |
| Counts | How many organizations, users, agents and assets |
| Public URL | Your instance's address — always sent, see below |
| Timestamps | When the process started, and when it reported |
Counts, not contents. No names, no email addresses, no conversation text, no asset inventory, no credentials, nothing about what you administer.
The public URL is the one field that names your network rather than measuring something, and on a licensed instance it is sent. It used to be a switch on the licence screen; it is not any more, because an installation the publisher can identify only by a fingerprint is one where "which of these is the customer calling about" has no answer. The screen now shows you the exact address that leaves, under Settings → Licence. If that is not acceptable for your deployment, the answer is to run unlicensed — which contacts nobody at all — or to take it up with your provider before installing.
The reply can carry a renewed key, which your instance adopts on its own — so a renewal reaches you without anybody re-pasting anything.
Your platform does not stop working because of a licence. Expiry gives you a grace period, then refuses only the creation of new organizations, users and agents — everything already set up keeps running, and so does the assistant. There is no state in which Jarvis disables, deletes or locks you out of something you are already using. If the check-in cannot reach the server, nothing changes at all: the key you hold is what governs, and it is checked without a network.
Licence
The images are provided as-is with no warranty, no support and no commitment to future availability. The source is not public and no rights to it are granted. Ask the maintainer before deploying this commercially or for third parties.