The self-hosting stack
This commit is contained in:
+154
@@ -0,0 +1,154 @@
|
||||
# Jarvis — configuration. Copy to `.env` beside the compose file and fill in.
|
||||
#
|
||||
# cp .env.example .env
|
||||
#
|
||||
# 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.
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 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.
|
||||
JARVIS_PORT=8080
|
||||
|
||||
# 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.
|
||||
#
|
||||
# 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=
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The model
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# 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_MODEL=gpt-4o
|
||||
|
||||
# minimal | low | medium | high. Higher costs latency and tokens and is worth it for real work.
|
||||
#OPENAI_THINKING_LEVEL=medium
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Channels and pinning (optional, recommended in production)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Every image tracks the `stable` channel unless you set these, so `docker compose pull && up -d`
|
||||
# upgrades you to whatever has most recently been promoted. `stable` moves only after a build has
|
||||
# run on the publisher's own instance; `dev` moves on every build and nothing has tried it yet.
|
||||
# `latest` is a second name for `stable`, kept so nothing that already used it has to change.
|
||||
#
|
||||
# Which channel you are on is yours to state, because nothing in the image knows it — a channel is
|
||||
# decided after a build and moves afterwards. Set it and the app shows it beside the version
|
||||
# numbers; leave it empty if you pin below, because then you follow no channel.
|
||||
#JARVIS_CHANNEL=stable
|
||||
|
||||
# Pinning makes an upgrade a decision instead of a side effect of pulling. The app reports its real
|
||||
# version either way — a channel tag is a second name on the same image, not a build that forgot
|
||||
# its number.
|
||||
#
|
||||
# THERE IS NO EXAMPLE NUMBER HERE ON PURPOSE. Pin the version you are ALREADY RUNNING, which the app
|
||||
# footer shows as `web … · api …`. Nothing in the publishing path bumps a number written into this
|
||||
# file, so any number printed here is one that went stale while nobody was looking — and moving the
|
||||
# api pin BACKWARDS runs an old build against a schema that has already been migrated forward.
|
||||
#JARVIS_IMAGE_API=git.luxit.be/luxit/jarvis-api:<the api version in your footer>
|
||||
#JARVIS_IMAGE_WEB=git.luxit.be/luxit/jarvis-web:<the web version in your footer>
|
||||
|
||||
# Only read when the agent overlay is enabled, just below. Its version is the AGENT's, and moves
|
||||
# independently of the two above — a Jarvis release usually does not change the agent at all.
|
||||
#
|
||||
# PIN THIS ONE FIRST if you pin only one. The two above change what your own server runs; this one
|
||||
# changes what runs on every machine you administer. Pinning it decides which build your instance
|
||||
# publishes; a separate UPDATE POLICY decides when a machine takes it, and its default is "let the
|
||||
# agent decide" — which in practice means the next time its service starts. Set that policy
|
||||
# instance-wide under Settings → Platform → General, per organization under Settings → Organization →
|
||||
# Agent updates. An enrolled agent also refuses any version that is not strictly newer, so moving
|
||||
# this back stops a rollout rather than reversing it on machines that already took the update.
|
||||
#
|
||||
# The app footer does not carry this number. Each enrolled machine reports the build it runs, on the
|
||||
# Agents page — that is the one to pin.
|
||||
#JARVIS_IMAGE_AGENT=git.luxit.be/luxit/jarvis-agent-dist:<version>
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# The Jarvis agent (optional)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Enrolling a machine downloads a compiled binary, which the api serves from a directory it can
|
||||
# only read. `docker-compose.agent.yml` supplies that directory as a pullable image, so the release
|
||||
# arrives the same way the rest of the stack does. Uncomment this and every later `docker compose`
|
||||
# command picks up both files with no extra flags:
|
||||
#
|
||||
#COMPOSE_FILE=docker-compose.yml:docker-compose.agent.yml
|
||||
#
|
||||
# Leaving it off is a supported state, not a broken one: everything except the agent works, and the
|
||||
# installer answers 503 saying no build is published. The SSH, Proxmox, Microsoft 365 and MikroTik
|
||||
# connectors all reach machines without it.
|
||||
#
|
||||
# AGENT_RELEASE_DIR is set by that overlay and should NOT be set here — a value in this file would
|
||||
# point the api at a path nothing populates, turning the honest 503 into a 404 per platform.
|
||||
|
||||
#AGENT_HEARTBEAT_INTERVAL_SEC=30
|
||||
#RUN_SHUTDOWN_GRACE_SEC=25
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Licence (required to create anything)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Your licence key. REQUIRED to create anything new. Ask antoine@luxit.be for one.
|
||||
#
|
||||
# Without it, an instance keeps running everything already set up and refuses to create new
|
||||
# organizations, users, agents and assets — so an existing deployment does not stop working when
|
||||
# this reaches it, and a fresh install gets as far as its first administrator and then needs a key.
|
||||
# An unlicensed instance also contacts nobody at all. See the README for exactly what a licensed one
|
||||
# reports.
|
||||
#
|
||||
# This value SEEDS the database on first boot and does not govern it afterwards: a renewal arrives
|
||||
# through the check-in and is stored, so leaving an old key here cannot roll you back.
|
||||
#JARVIS_LICENSE_KEY=
|
||||
@@ -0,0 +1,13 @@
|
||||
# These files are read by Docker on a Linux host, whatever the machine that cloned them.
|
||||
#
|
||||
# Without this, a clone on Windows checks them out with CRLF, and the carriage return rides into
|
||||
# `.env` as part of a VALUE — Compose passes it through verbatim, so it ends up inside the database
|
||||
# password, the JWT secrets and the vault master key. The failure then surfaces as Postgres refusing
|
||||
# the connection, or as a vault that cannot decrypt what it wrote yesterday, with nothing anywhere
|
||||
# naming a line ending as the cause.
|
||||
* text=auto eol=lf
|
||||
|
||||
# And the screenshots are bytes, not text. `text=auto` above already detects that correctly, but
|
||||
# saying so costs one line and removes the question before somebody adds a JPEG.
|
||||
*.png binary
|
||||
*.jpg binary
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
# Your instance's configuration, which holds every secret it has: the vault master key, the JWT
|
||||
# secrets, the database password and your AI provider's API key. NEVER commit it. `.env.example` is
|
||||
# the template and is the only one of these that belongs in a repository.
|
||||
.env
|
||||
.env.*
|
||||
!.env.example
|
||||
|
||||
# Database dumps, which the README tells you to take before an upgrade. They contain every
|
||||
# conversation, asset and audit row on the instance — and the vault's ciphertext, which is one
|
||||
# leaked .env away from being plaintext.
|
||||
*.sql
|
||||
*.sql.gz
|
||||
@@ -0,0 +1,478 @@
|
||||
<p align="center">
|
||||
<img src="jarvis.svg" width="76" alt="Jarvis">
|
||||
</p>
|
||||
|
||||
<h1 align="center">Jarvis</h1>
|
||||
|
||||
<p align="center"><strong>AI-assisted infrastructure administration for MSPs.</strong></p>
|
||||
|
||||
An operator opens a conversation and asks for the work in words. Jarvis does it: a command over SSH
|
||||
or through an enrolled agent, a Proxmox guest powered on, a Microsoft 365 account blocked, a MikroTik
|
||||
firewall read back — on the machines of one client organization, with credentials it decrypts from
|
||||
the vault and shows nobody. Every call is judged before it runs, and the ones that cannot be undone
|
||||
stop and ask a human in the thread.
|
||||
|
||||
The difference from an assistant that writes commands for you to paste is that these run.
|
||||
|
||||
This repository runs Jarvis from published container images — no source, no build, no account with
|
||||
the project.
|
||||
|
||||
- [What it does](#what-it-does)
|
||||
- [What decides whether a tool call runs](#what-decides-whether-a-tool-call-runs)
|
||||
- [What it looks like](#what-it-looks-like)
|
||||
- [How it works](#how-it-works)
|
||||
- [What you need](#what-you-need)
|
||||
- [Install](#install)
|
||||
- [Your reverse proxy has two requirements](#your-reverse-proxy-has-two-requirements)
|
||||
- [Upgrading](#upgrading)
|
||||
- [The agent](#the-agent)
|
||||
- [Things worth knowing before you trust it with production](#things-worth-knowing-before-you-trust-it-with-production)
|
||||
- [When it does not come up](#when-it-does-not-come-up)
|
||||
- [Backups, and restoring one](#backups-and-restoring-one)
|
||||
- [Removing it](#removing-it)
|
||||
- [Licence keys, and what your instance reports](#licence-keys-and-what-your-instance-reports)
|
||||
- [Getting a licence, and getting help](#getting-a-licence-and-getting-help)
|
||||
|
||||
## What it does
|
||||
|
||||
**Five connectors reach a managed system.** Each one is a set of tools the assistant may call, and
|
||||
what it may do with them is decided per call — see the next section.
|
||||
|
||||
| Connector | Reaches |
|
||||
| --- | --- |
|
||||
| **SSH** | Anything with a shell — Linux, Windows, and CLI-driven network gear. An appliance that serves no SFTP is offered the command tool alone, rather than four that would fail at the handshake. |
|
||||
| **Jarvis agent** | A machine running the enrolled agent, which dials out — so it works behind NAT, on a dynamic address, with no inbound rule and no SSH exposed. Commands, files, services, processes, system facts. |
|
||||
| **Proxmox VE** | The cluster API. Read and power only — no create, clone, snapshot, backup or migrate. |
|
||||
| **MikroTik RouterOS** | The native binary API, or the RouterOS 7 REST API. One identical tool surface either way; the connection decides the transport. |
|
||||
| **Microsoft 365** | Graph, app-only, on any Microsoft cloud, authenticating with a client secret or a certificate. Named tools for identity, licences, groups, admin roles, Exchange Online, Intune and the audit logs, plus one that reaches every remaining Graph endpoint. |
|
||||
|
||||
**An asset is the managed thing; a connection is a way of reaching it.** A Linux host commonly
|
||||
answers on OpenSSH *and* through an enrolled agent; a Proxmox node has a cluster API, a shell and an
|
||||
agent. Each route carries its own address, its own credentials and its own health — so a vendor API
|
||||
that stops answering no longer makes a device unmanageable while its console is up.
|
||||
|
||||
**Documents.** A conversation accumulates what the assistant writes: reports and runbooks in
|
||||
Markdown, diagrams in Mermaid, tabular data as a workbook. What it writes is always a *source*, and
|
||||
Jarvis renders the file on download — PDF and Word from prose, Excel and CSV from a workbook, HTML
|
||||
from either. Keeping the source is what leaves a document revisable instead of a dead binary.
|
||||
|
||||
**And what an MSP has to administer about itself**: organizations and members under a role ceiling,
|
||||
invitations, an encrypted vault with its own folder tree and its own grants, an audit trail,
|
||||
passkeys and TOTP, a notification centre, and a console in English, French or German — a property of
|
||||
the account, not of the browser.
|
||||
|
||||
## What decides whether a tool call runs
|
||||
|
||||
Three things, resolved on every single call. The most restrictive wins.
|
||||
|
||||
**Risk is a property of the operation.** A tool declares a baseline and may escalate per invocation:
|
||||
a shell command is mutating in general and destructive for `rm -rf`, `mkfs`, `shutdown`,
|
||||
`iptables -F` and about two dozen other patterns. Escalation is one-way — a per-call assessment can
|
||||
raise the risk, never lower it.
|
||||
|
||||
**Autonomy is a property of the conversation**, chosen by the operator:
|
||||
|
||||
| | Safe | Mutating | Destructive |
|
||||
| --- | --- | --- | --- |
|
||||
| **Read-only** | run | refuse | refuse |
|
||||
| **Ask before every change** | run | ask | ask |
|
||||
| **Ask before destructive changes** (default) | run | run | ask |
|
||||
| **Full access** | run | run | run |
|
||||
|
||||
"Ask" raises an approval request in the conversation, and the run parks until a human answers. The
|
||||
level is re-read on every call, so lowering it takes effect on the very next tool call of a run
|
||||
already in flight. **Full access removes the last in-chat gate for every participant** — destructive
|
||||
operations then run immediately, with no prompt and no second pair of eyes. Make it a deliberate
|
||||
choice.
|
||||
|
||||
**Permission is a property of the person.** A grant says which slice of the asset tree somebody may
|
||||
operate, through which connectors, up to which risk, and whether the tools whose operation the
|
||||
assistant *composes* — a shell command, an arbitrary Graph request, the contents of a file — are
|
||||
admitted at all. Grants resolve by walking outward from the asset: the most specific level that says
|
||||
anything decides entirely, and if nothing has spoken by the root the answer is no. Absent means
|
||||
nothing, so a forgotten grant fails closed rather than open.
|
||||
|
||||
> The conversation's autonomy is a **floor the operator imposes on themselves**; the grant is the
|
||||
> **ceiling imposed on them**. What runs is whichever binds.
|
||||
|
||||
A conversation also carries a **scope** — any mix of assets and folders, a folder granting its whole
|
||||
subtree. Scope is checked *before* the asset is resolved, so a machine out of scope never has its
|
||||
vault secrets decrypted.
|
||||
|
||||
## What it looks like
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/img/console.png" alt="The Jarvis console, on a freshly installed instance" width="900">
|
||||
</p>
|
||||
|
||||
The console on a fresh install. The footer names the two builds you are running, which is the first
|
||||
thing to quote when something is wrong.
|
||||
|
||||
<p align="center">
|
||||
<img src="docs/img/asset.png" alt="One asset, its two routes, its agent and its inventory" width="900">
|
||||
</p>
|
||||
|
||||
One asset, and the distinction the model rests on: **two routes to the same machine**, each with its
|
||||
own address, its own credentials and its own health. One is preferred and untested; the other is
|
||||
disabled without being deleted, so its settings and its history survive and no tool may use it.
|
||||
|
||||
## How it works
|
||||
|
||||
Four containers. **Only `web` publishes a port**: its nginx serves the console and reverse-proxies
|
||||
`/api` and the websocket to `api` over the internal network, so your TLS terminator has exactly one
|
||||
target and the API is never reachable from outside the compose network.
|
||||
|
||||
| Service | What it is |
|
||||
| --- | --- |
|
||||
| `web` | nginx serving the React console. The only published port. |
|
||||
| `api` | REST, auth, the vault, the connectors, the tool-calling loop, and the websocket. |
|
||||
| `postgres` | Everything except the vault master key. |
|
||||
| `redis` | Socket fan-out, nonces and rate-limit counters. |
|
||||
|
||||
The assistant streams a turn, executes the tool calls the model asked for **strictly after the
|
||||
stream is fully drained**, then feeds the results back — up to fifty rounds per user turn. That
|
||||
ordering is what makes retrying a broken stream safe: at the moment a stream fails, no tool of that
|
||||
round has run, so replaying it re-generates intent and never re-runs an operation.
|
||||
|
||||
**A run that was in flight when the api stopped is picked up when it comes back.** A shutdown aborts
|
||||
each loop and lets it write out what it had streamed with a note saying why the transcript ends
|
||||
there; the next process finishes that message, closes any tool call whose outcome is unknown saying
|
||||
in as many words that it is unknown, and resumes the run — instructed to read the current state
|
||||
before repeating anything that writes. This is why the api asks for a stop grace period, and part of
|
||||
why it must run as a single replica.
|
||||
|
||||
**The agent dials out.** Nothing inbound is opened on a managed machine. It holds a websocket to
|
||||
your instance, signs each session with a key whose private half never leaves it, and reports its
|
||||
inventory on every heartbeat.
|
||||
|
||||
## What you need
|
||||
|
||||
- Docker with Compose v2, on **x86-64 Linux**. The api and web images are published for
|
||||
`linux/amd64` only, so an arm64 host — a Pi, an Ampere, a Graviton — fails at `docker compose pull`
|
||||
with no matching manifest. The machines you *administer* have no such limit: the agent ships arm64
|
||||
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.
|
||||
- A licence key. See [Licence keys](#licence-keys-and-what-your-instance-reports) — without one an
|
||||
instance runs but creates nothing new.
|
||||
- Roughly 2 GB of RAM for the stack and room for Postgres to grow.
|
||||
|
||||
## 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
|
||||
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.
|
||||
|
||||
**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.
|
||||
|
||||
**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.** Two separate sockets ride `JARVIS_PORT`: the chat, on
|
||||
`/socket.io/`, and enrolled agents, on `/api/agents/ws`. Neither falls back to plain HTTP. Without
|
||||
the upgrade the chat does not lose streaming — it never connects at all, and since the prompt
|
||||
itself travels over that socket, nothing sends. Presence and in-chat approvals go with it, 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.
|
||||
|
||||
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.
|
||||
|
||||
## Upgrading
|
||||
|
||||
```sh
|
||||
docker compose pull && docker compose up -d
|
||||
```
|
||||
|
||||
That is the whole upgrade: the api, the web, and — with the agent overlay on — the agent release all
|
||||
track the **`stable`** channel by default. Postgres and Redis are not on a Jarvis channel; they
|
||||
follow their own upstream tags, `postgres:16-alpine` and `redis:7-alpine`. 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.
|
||||
|
||||
**A schema change is one-way.** Jarvis has no migration history: each boot force-syncs the database
|
||||
to the schema its image carries, adding what a release added and dropping what it removed, without
|
||||
prompting. Pulling an older api image does not undo that — unlike the image itself, the schema stays
|
||||
where the newer build left it. Restoring a dump is the only way back, which is what makes the one
|
||||
below a prerequisite rather than a precaution.
|
||||
|
||||
```sh
|
||||
docker compose exec -T postgres pg_dump -U jarvis jarvis | gzip > jarvis-$(date +%F).sql.gz
|
||||
```
|
||||
|
||||
### Channels
|
||||
|
||||
| Channel | What it means |
|
||||
| -------- | ---------------------------------------------------------------------------------- |
|
||||
| `stable` | Promoted after running on the publisher's own instance. **The default, and what you want.** |
|
||||
| `dev` | Every build, as soon as it is published. Nothing has tried it yet. |
|
||||
| `latest` | A second name for `stable`, kept so nothing that already used it has to change. |
|
||||
|
||||
A channel is a **pointer** and a version number is a **fact**. `0.78.2` means one specific set of
|
||||
bytes for ever; `stable` means whichever set we currently stand behind, and it moves. A build only
|
||||
reaches `stable` by being promoted — and promotion copies the manifest of an image that has already
|
||||
been published and already run. It never rebuilds, so the bytes you receive are the same bytes that
|
||||
were tested, not a fresh build of the same source.
|
||||
|
||||
**Channels do not mean the app forgets which build it is.** The version is stamped into the image
|
||||
when it is built, so the footer in the app, `/version.json` and the agent manifest 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. Set `JARVIS_CHANNEL` in `.env` and the footer names your channel
|
||||
beside those numbers; leave it empty if you pin, because then you follow no channel.
|
||||
|
||||
Once you are in production, consider pinning: set `JARVIS_IMAGE_API`, `JARVIS_IMAGE_WEB` and
|
||||
`JARVIS_IMAGE_AGENT` in `.env` to explicit version tags. It makes an upgrade a decision rather than a
|
||||
side effect of pulling. Pin the version you are *already running* — the footer shows it — rather than
|
||||
one copied from a document, and remember that moving the api pin backwards runs an old build against
|
||||
a schema that has already moved forward.
|
||||
|
||||
**Pin the agent one first if you pin only one.** The api and the web change what your own server
|
||||
runs; the agent changes what runs on every machine you administer. Pinning the image decides which
|
||||
build your instance publishes; a separate **update policy** decides when a machine takes it —
|
||||
*As soon as available*, *On a schedule*, *Manually only*, *Let the agent decide* (the default, which
|
||||
means on its next service start, and on a server that can be months) or *Never*. The instance-wide
|
||||
answer is on **Settings → Platform → General**; an organization overrides it under **Settings →
|
||||
Organization → Agent updates**, and a single machine overrides that. An enrolled agent also refuses
|
||||
any version that is not strictly newer, so moving that pin back stops a rollout rather than reversing
|
||||
it on machines that already took the update.
|
||||
|
||||
## The agent
|
||||
|
||||
**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 manifest and download endpoints
|
||||
answer 503 saying no build is published, and the SSH, Proxmox, Microsoft 365 and MikroTik connectors
|
||||
all reach machines without it.
|
||||
|
||||
Turn it on by adding one line to `.env`, so that every later `docker compose` command picks up both
|
||||
files with no extra flags:
|
||||
|
||||
```sh
|
||||
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, the **Agents** page in the sidebar issues
|
||||
the install command: click **New install command**, pick Linux, macOS or Windows, and copy the one
|
||||
line. (Settings → Organization → Agent updates is a different screen — it schedules how
|
||||
already-enrolled agents take new builds.)
|
||||
|
||||
Four 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 re-checks the file on disk on every download
|
||||
request and re-hashes it whenever its size or timestamp has changed, so a new release in the volume
|
||||
is served immediately and the published checksum always describes the bytes actually being served.
|
||||
- **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. The app footer does not carry it either — each enrolled
|
||||
machine reports the build it runs, on the Agents page. Pin it with `JARVIS_IMAGE_AGENT`.
|
||||
- **It carries an interactive shell.** Beyond what the assistant can do with it, a person gets a real
|
||||
terminal on an enrolled machine from the browser — Linux, macOS and Windows alike. Sessions are
|
||||
recorded by default, encrypted under `VAULT_MASTER_KEY` and deleted on a retention policy you set
|
||||
under **Settings → Organization → Terminal sessions**. Turning recording off stops the transcript,
|
||||
never the audit entry.
|
||||
|
||||
**What an enrolled agent can do.** Inventory the machine, run commands, read, write and fetch files,
|
||||
list and control services, list processes, update itself, and carry that shell. It runs as root on
|
||||
Linux and macOS and as LocalSystem on Windows, deliberately — its purpose is to administer the
|
||||
machine.
|
||||
|
||||
What the **assistant** does with that reach is bounded by the autonomy policy and the approval gates
|
||||
above. **The interactive shell is not.** There is no command to inspect before a shell opens, so the
|
||||
risk ceiling has nothing to weigh; it is gated instead by a permission and a per-machine switch on
|
||||
the grant. Decide who holds those 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_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.
|
||||
- **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
|
||||
websocket of every enrolled agent live in one process's memory. The Redis in this stack does not
|
||||
lift that limit: it fans outgoing events out to other replicas, but an incoming one is only ever
|
||||
handled by the replica holding that connection. So a cancel or an approval answered on the wrong
|
||||
replica is silently dropped, and an agent tool call can land on a replica that does not hold the
|
||||
target machine. Worse, a starting replica's recovery sweep claims every run it does not own — so a
|
||||
second instance re-executes, against your real infrastructure, operations the first is still
|
||||
running.
|
||||
- **There is no rate limiting on sign-in.** No throttler, no account lockout, and authentication
|
||||
events are not audited. Credential stuffing is bounded only by the reverse proxy you put in front,
|
||||
which this repository does not ship. If your proxy can rate-limit one route, make it that one.
|
||||
- **An access token dies with its session, with one exception.** Every access token carries the id of
|
||||
the session that issued it and every request re-checks that the session is live, so revoking a
|
||||
session, signing other devices out or deactivating an account cuts that token off on its next
|
||||
request. The exception is changing your own password: it revokes every *other* session and keeps
|
||||
the one you are changing it from, so a token stolen from that session stays valid until it expires
|
||||
— 15 minutes by default (`JWT_ACCESS_TTL`). An admin-forced reset drops every session.
|
||||
- **The api container runs as root**, and so does nginx's master process in the web container,
|
||||
though its workers drop privileges. Neither image declares a `USER`.
|
||||
- **`/api/health` answers 200 with `status: "degraded"`** when the database is unreachable, so the
|
||||
container healthcheck alone is not a liveness signal for the database.
|
||||
- **Outbound mail is Microsoft Graph only.** There is no SMTP and no environment variable for any of
|
||||
it: an app registration with `Mail.Send` and a shared mailbox, set up on the **Mail** tab of
|
||||
**Settings → Platform → General**. Without it, invitations still work — the link comes back to the
|
||||
admin who created it instead of being emailed. Nothing else is emailed: there is no password reset
|
||||
and no address verification.
|
||||
- The assistant executes real operations on real infrastructure. What stops for a human is the
|
||||
conversation's autonomy level, and at *Full access* nothing does. See
|
||||
[What decides whether a tool call runs](#what-decides-whether-a-tool-call-runs).
|
||||
|
||||
## When it does not come up
|
||||
|
||||
```sh
|
||||
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`:
|
||||
|
||||
| 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. |
|
||||
| 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 nothing can be created | No licence key. See below. |
|
||||
| An agent installer answers 503 | No agent build is published — the overlay is off. That is a supported state. |
|
||||
|
||||
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.
|
||||
|
||||
## 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`.
|
||||
|
||||
```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.
|
||||
|
||||
To restore, stop the api so nothing writes while you work, then load the dump into an empty database:
|
||||
|
||||
```sh
|
||||
docker compose stop api web
|
||||
docker compose exec -T postgres psql -U jarvis -d postgres \
|
||||
-c 'DROP DATABASE IF EXISTS jarvis;' -c 'CREATE DATABASE jarvis;'
|
||||
gunzip -c jarvis.sql.gz | docker compose exec -T postgres psql -U jarvis -d jarvis
|
||||
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.
|
||||
|
||||
## Removing it
|
||||
|
||||
```sh
|
||||
docker compose down # stops everything, keeps the data
|
||||
docker compose down -v # also deletes the volumes — every conversation, asset and credential
|
||||
```
|
||||
|
||||
`down -v` is not recoverable from anything but a dump you already took.
|
||||
|
||||
## Licence keys, and what your instance reports
|
||||
|
||||
**Jarvis needs a licence key.** Ask for one (see [below](#getting-a-licence-and-getting-help)) 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, every asset, and the assistant itself — and refuses only to create NEW ones: no new
|
||||
organization, user, agent or asset. 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 |
|
||||
| Signature + nonce | Proof the message came from this instance, and a one-time value so an old one cannot be replayed. Carries nothing about you. |
|
||||
|
||||
**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 shows you the exact address that leaves,
|
||||
under **Settings → Platform → 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 us 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 — as
|
||||
long as your key says, which on the current plans is 30 days, and none at all on a trial. After it,
|
||||
Jarvis refuses only the creation of new organizations, users, agents and assets; 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.
|
||||
|
||||
## Getting a licence, and getting help
|
||||
|
||||
**Antoine Cavelier — <antoine@luxit.be>.** Licence keys, pricing, and anything wrong with the
|
||||
product.
|
||||
|
||||
When something is broken, the two facts worth putting in the first message are the build you are on
|
||||
and what the api said:
|
||||
|
||||
```sh
|
||||
curl -s https://your-jarvis.example.com/version.json # the web and api versions
|
||||
docker compose logs --tail=100 api
|
||||
```
|
||||
|
||||
`/version.json` is public on purpose, so you can quote it without signing in.
|
||||
|
||||
## 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 before deploying this commercially or
|
||||
for third parties.
|
||||
@@ -0,0 +1,45 @@
|
||||
# Jarvis — the agent release, as a pullable image.
|
||||
#
|
||||
# OPTIONAL OVERLAY. The base stack runs perfectly without it; what it adds is the one feature a
|
||||
# self-hosted instance cannot otherwise have, because enrolling a machine downloads a compiled
|
||||
# binary and there is nowhere for a compose-only deployment to get one. This carries that release
|
||||
# as an OCI image, so it arrives through the same `docker compose pull` as the api and the web.
|
||||
#
|
||||
# Turn it on by naming both files. Either spelling works:
|
||||
#
|
||||
# docker compose -f docker-compose.yml -f docker-compose.agent.yml up -d
|
||||
#
|
||||
# or, so that a plain `docker compose ...` keeps working for every later command, put this in .env:
|
||||
#
|
||||
# COMPOSE_FILE=docker-compose.yml:docker-compose.agent.yml
|
||||
#
|
||||
# Upgrading is unchanged: `docker compose pull && docker compose up -d`. The publisher re-runs,
|
||||
# replaces the release in the volume, and the api picks it up WITHOUT a restart — digests are
|
||||
# computed from the bytes on disk on every request, not cached at boot.
|
||||
services:
|
||||
# Runs once per `up`, copies its payload into the shared volume, exits. Not a server.
|
||||
agent-releases:
|
||||
image: ${JARVIS_IMAGE_AGENT:-git.luxit.be/luxit/jarvis-agent-dist:stable}
|
||||
# Explicit, because the default would be wrong the moment somebody copies this block: a
|
||||
# restarting one-shot is an infinite loop, and Compose's own default policy is already "no".
|
||||
restart: "no"
|
||||
volumes:
|
||||
- agent_releases:/out
|
||||
|
||||
api:
|
||||
# NOTE THE COUPLING: until the publisher has exited 0, the api does not start. That is
|
||||
# deliberate — a release that failed to arrive should stop the deploy and say so, rather than
|
||||
# leave an instance quietly handing 404s to every installer somebody runs this week. The cost
|
||||
# is that an unreachable registry now blocks the whole stack, so if that trade is wrong for
|
||||
# you, drop these three lines and the api will simply serve no build until the volume fills.
|
||||
depends_on:
|
||||
agent-releases:
|
||||
condition: service_completed_successfully
|
||||
volumes:
|
||||
# Read-only: the API serves these bytes to every managed machine and never writes here.
|
||||
- agent_releases:/srv/agent-releases:ro
|
||||
environment:
|
||||
AGENT_RELEASE_DIR: /srv/agent-releases
|
||||
|
||||
volumes:
|
||||
agent_releases:
|
||||
@@ -0,0 +1,155 @@
|
||||
# Jarvis — self-hosting stack.
|
||||
#
|
||||
# Everything runs from published images; nothing is built here and no source is needed.
|
||||
#
|
||||
# cp .env.example .env # then fill it in — see the comments in that file
|
||||
# docker compose pull
|
||||
# docker compose up -d
|
||||
#
|
||||
# Only the `web` service publishes a port. Its nginx serves the app and reverse-proxies /api and the
|
||||
# websocket to the internal `api` service, so your own TLS terminator has exactly one target and the
|
||||
# API is never reachable from outside this compose network.
|
||||
#
|
||||
# UPGRADING: `docker compose pull && docker compose up -d`. Every Jarvis image here tracks `stable` by
|
||||
# default, so that is the whole upgrade. Postgres and Redis are not on a Jarvis channel — they follow
|
||||
# their own upstream tags. Schema changes apply themselves when the api starts, and they are ONE-WAY:
|
||||
# there is no migration history, so pulling an older api image does not put the schema back. Take a
|
||||
# dump first. See the README.
|
||||
#
|
||||
# A channel tag does NOT mean the app forgets which build it is: `stable` is a second name on the same
|
||||
# image as its version tag, and the version is stamped INTO the image when it is built. The footer in
|
||||
# the app and /version.json keep reporting the real number whichever name you pulled it under — which
|
||||
# is what lets you tell somebody which build you are on when something goes wrong.
|
||||
#
|
||||
# To pin instead — recommended once you are in production, because it makes an upgrade a decision rather
|
||||
# than a side effect of pulling — set JARVIS_IMAGE_API and JARVIS_IMAGE_WEB (and JARVIS_IMAGE_AGENT, if
|
||||
# you run the agent overlay) in .env to explicit version tags.
|
||||
#
|
||||
# No version number is written in this comment on purpose. Nothing in the publishing path would ever
|
||||
# bump one, so a number here is a number that goes stale while nobody is looking.
|
||||
name: jarvis
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_USER: jarvis
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?set POSTGRES_PASSWORD in .env}
|
||||
POSTGRES_DB: jarvis
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U jarvis -d jarvis"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
restart: unless-stopped
|
||||
# Append-only so a restart does not lose the queue and the socket fan-out state.
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
|
||||
api:
|
||||
image: ${JARVIS_IMAGE_API:-git.luxit.be/luxit/jarvis-api:stable}
|
||||
restart: unless-stopped
|
||||
# The assistant runs long operations, and a deploy is the most common thing that interrupts one.
|
||||
# Given room to stop, the API aborts each loop, writes the partial answer with a note saying why
|
||||
# the transcript ends there, and marks the run interrupted so the next process picks it up.
|
||||
# Docker's 10s default is not enough. RUN_SHUTDOWN_GRACE_SEC must stay the smaller of the two.
|
||||
stop_grace_period: 60s
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
NODE_ENV: production
|
||||
API_PORT: "4000"
|
||||
|
||||
# The address a browser reaches Jarvis on. This is the CORS decision: requests from any other
|
||||
# origin are refused, so a wrong value here presents as a sign-in page that cannot sign in.
|
||||
WEB_ORIGIN: ${WEB_ORIGIN:?set WEB_ORIGIN in .env}
|
||||
# The address baked into agent install commands and dialled by every enrolled machine. Usually
|
||||
# the same string as WEB_ORIGIN; separate because they answer different questions, and a wrong
|
||||
# value here does not fail at deploy time — it fails weeks later, on somebody else's server.
|
||||
PUBLIC_URL: ${PUBLIC_URL:-${WEB_ORIGIN}}
|
||||
|
||||
DATABASE_URL: postgresql://jarvis:${POSTGRES_PASSWORD}@postgres:5432/jarvis?schema=public
|
||||
REDIS_URL: redis://redis:6379
|
||||
|
||||
JWT_ACCESS_SECRET: ${JWT_ACCESS_SECRET:?set JWT_ACCESS_SECRET in .env}
|
||||
JWT_REFRESH_SECRET: ${JWT_REFRESH_SECRET:?set JWT_REFRESH_SECRET in .env}
|
||||
JWT_ACCESS_TTL: "900"
|
||||
JWT_REFRESH_TTL: "1209600"
|
||||
|
||||
# READ THE NOTE IN .env.example BEFORE CHANGING THIS. Every credential in the vault is
|
||||
# encrypted under it; lose it and they are gone, with no recovery of any kind.
|
||||
VAULT_MASTER_KEY: ${VAULT_MASTER_KEY:?set VAULT_MASTER_KEY in .env}
|
||||
|
||||
# Any OpenAI-compatible endpoint. Defaults to OpenAI itself.
|
||||
OPENAI_BASE_URL: ${OPENAI_BASE_URL:-https://api.openai.com/v1}
|
||||
OPENAI_API_KEY: ${OPENAI_API_KEY:?set OPENAI_API_KEY in .env}
|
||||
OPENAI_MODEL: ${OPENAI_MODEL:-gpt-4o}
|
||||
OPENAI_THINKING_LEVEL: ${OPENAI_THINKING_LEVEL:-medium}
|
||||
|
||||
# How many proxies sit in front and rewrite X-Forwarded-For. ONE is the web container's own
|
||||
# nginx, which is always there — so 1 is right when nothing else fronts it, and 2 when your own
|
||||
# TLS terminator does. Raising it is the dangerous direction: the API trusts that many hops of a
|
||||
# header the client can forge, and too high lets a caller choose the IP that lands in the audit
|
||||
# log, in the session list and in the enrollment rate limit.
|
||||
TRUST_PROXY_HOPS: ${TRUST_PROXY_HOPS:-2}
|
||||
|
||||
# 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:-}
|
||||
# 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
|
||||
# decision — and the footer shows nothing rather than a label that stopped being true.
|
||||
#
|
||||
# It is not baked into the image, and cannot be: a channel is decided after a build and moves
|
||||
# afterwards, so the same image is `dev` one week and `stable` the next. Only you know which
|
||||
# one you are on.
|
||||
APP_CHANNEL: ${JARVIS_CHANNEL:-}
|
||||
|
||||
AGENT_HEARTBEAT_INTERVAL_SEC: ${AGENT_HEARTBEAT_INTERVAL_SEC:-30}
|
||||
RUN_SHUTDOWN_GRACE_SEC: ${RUN_SHUTDOWN_GRACE_SEC:-25}
|
||||
|
||||
# Where the agent binaries live, if you have them. Leaving this unset is a supported state:
|
||||
# everything except the agent installer works, and the installer answers 503 saying no build is
|
||||
# published. See the README — a self-hosted instance has no way to produce these.
|
||||
AGENT_RELEASE_DIR: ${AGENT_RELEASE_DIR:-}
|
||||
healthcheck:
|
||||
test:
|
||||
- CMD
|
||||
- node
|
||||
- -e
|
||||
- "fetch('http://localhost:4000/api/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))"
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 12
|
||||
# First boot syncs the schema and runs the data backfills before it listens.
|
||||
start_period: 40s
|
||||
|
||||
web:
|
||||
image: ${JARVIS_IMAGE_WEB:-git.luxit.be/luxit/jarvis-web:stable}
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
- api
|
||||
ports:
|
||||
# Put your own TLS terminator in front of this. Jarvis speaks plain HTTP here on purpose and
|
||||
# reads X-Forwarded-Proto to know what the browser actually used.
|
||||
- "${JARVIS_PORT:-8080}:80"
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
redis_data:
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 114 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 252 KiB |
+40
@@ -0,0 +1,40 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96" width="96" height="96" role="img" aria-label="Jarvis">
|
||||
<!--
|
||||
The Jarvis identity mark. Two hexagonal shells OUT OF PHASE — a flat-top holding a pointy-top —
|
||||
around a core. That phase offset is the signature: a version that drops the inner ring is a
|
||||
hexagon, not this logo.
|
||||
|
||||
Colours are literals rather than CSS variables, because a README is rendered by a forge that
|
||||
does not load the app's stylesheet. The stroke reads on a white and on a dark ground alike,
|
||||
which is what a mark in a README has to survive.
|
||||
-->
|
||||
<defs>
|
||||
<linearGradient id="shell" gradientUnits="userSpaceOnUse" x1="48" y1="6" x2="48" y2="90">
|
||||
<stop offset="0" stop-color="#6e64d8" />
|
||||
<stop offset="1" stop-color="#6e64d8" stop-opacity=".76" />
|
||||
</linearGradient>
|
||||
<radialGradient id="halo" gradientUnits="userSpaceOnUse" cx="48" cy="48" r="30">
|
||||
<stop offset="0" stop-color="#a49cfc" />
|
||||
<stop offset=".55" stop-color="#a49cfc" stop-opacity=".32" />
|
||||
<stop offset="1" stop-color="#a49cfc" stop-opacity="0" />
|
||||
</radialGradient>
|
||||
</defs>
|
||||
|
||||
<circle cx="48" cy="48" r="30" fill="url(#halo)" opacity=".26" />
|
||||
<path
|
||||
d="M 48 23 L 26.35 35.5 L 26.35 60.5 L 48 73 L 69.65 60.5 L 69.65 35.5 Z"
|
||||
fill="none"
|
||||
stroke="#6e64d8"
|
||||
stroke-width="4"
|
||||
stroke-linejoin="round"
|
||||
opacity=".4"
|
||||
/>
|
||||
<path
|
||||
d="M 90 48 L 69 11.63 L 27 11.63 L 6 48 L 27 84.37 L 69 84.37 Z"
|
||||
fill="none"
|
||||
stroke="url(#shell)"
|
||||
stroke-width="6"
|
||||
stroke-linejoin="round"
|
||||
/>
|
||||
<circle cx="48" cy="48" r="8" fill="#a49cfc" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
Reference in New Issue
Block a user