Installing metor

Four ways, from the most convenient to the most manual. The prebuilt box image comes from the GitHub Container Registry: ghcr.io/metor-com/metor-box (tags latest = main, 0.4.0 etc. for releases, plus a short commit sha), built by .github/workflows/box-image.yml for linux/amd64 and linux/arm64.

Security first

Whoever gets into the interface can read every chat, drive every bot and open every bot’s terminal. The gateway inside the box (port 6010) therefore signs devices in itself (ADR-0012): no passwords - the first browser gets in with a one-time setup link, every further phone or browser is linked by QR code or pairing code from a device that is already signed in, and every device can be removed again. Native clients use the same session mechanism through bearer tokens. All paired devices have access to the Space; pairing is not a restricted guest role.

Current boundary: bots, runtime processes and management run as the same OS user. A bot with shell access can read runtime/connector credentials and change management files, including device sessions. 0600 permissions and tool-specific read restrictions do not prevent this. Use separate Spaces for mutually untrusted workloads; management isolation is still planned, not implemented. See SECURITY.md for scope and limitations.

For deployment:

Private hosted delivery

For operator-managed servers, the installer can use a preloaded image without registry credentials on the target. See private release delivery for the image-ID requirement, trusted transfer procedure and remaining release gates. Existing desktop setup/update paths still need registry access; this installer option does not change their behavior.

Desktop app: set up an existing VPS

Choose Connect a Space → On a server → Set up an existing server. Enter the server IP/hostname and SSH port, compare the displayed ED25519 fingerprint with the provider console, then enter a domain pointing to the server and choose Password or SSH key for root login. The app checks the server before offering Install and connect. It keeps the SSH connection for ten minutes while you review; passwords and key passphrases are never saved. If the provider requires changing the initial password, do that in its console first.

Use a fresh Ubuntu 22.04/24.04/26.04 or Debian 12/13 server, x86-64 or ARM64, with at least 2 vCPUs, 4 GB RAM and 12 GiB free disk (16 GB RAM / 40 GB disk recommended). Allow inbound SSH plus TCP 80 and 443. Both A and AAAA DNS records, when present, must point to this server. The chosen SSH login method must be enabled. For SSH key, choose your local private key file (not its .pub file), then enter its passphrase if encrypted. The matching public key must already be installed for root; the private key stays on your device. The app installs Docker, the matching metor release and Caddy, then pairs itself once HTTPS is reachable. Keep it open during setup.

Existing installations and other Docker workloads are rejected. Failed app installations can be retried with the same domain; volumes and the Space RAM allocation are retained. Nothing is automatically uninstalled on failure. Non-root users and custom reverse proxies are not supported by this app flow. The first Hetzner installation and subsequent use were confirmed by the user on 2026-09-11. Automated local tests now cover actual installer process termination at four stages, using a Docker test double, plus real SSH connections for passwords, private keys and connection loss. Full recovery with real Docker and other providers still needs live validation.

The app records installation phases on the server and replaces configuration files atomically. Retries preserve extra .env settings and persistent data. When setup reached the ready stage, Reconnect existing Space pairs the app without downloading an image or restarting the Space. A stopped or unhealthy completed Space needs its container problem resolved before reconnecting.

DNS is checked before installation, including stale AAAA records. The domain must point directly to addresses on the server; proxied DNS/NAT configurations need manual setup. HTTPS failures distinguish DNS, port 443 reachability, certificate verification and incorrect proxy responses. The app never disables TLS verification or changes firewall rules automatically. Port checks show reachability; they cannot identify which firewall or network component is responsible.

On a server originally created with an SSH key, resetting the root password may enable only provider-console login. SSH root password login must be enabled separately before choosing Password in this app flow. SSH key works without enabling password login. The app does not change the server’s SSH authentication policy.

curl -fsSL https://www.metor.com/install.sh | sudo bash

That address redirects to the installer of the newest release (the install.sh asset of the latest GitHub release). The development version, straight from the repository:

curl -fsSL https://raw.githubusercontent.com/metor-com/metor/main/deploy/install.sh | sudo bash

The installer asks for a domain (nothing else) and proposes <public-ip>.sslip.io as the default, a name that resolves to the server without owning a domain (see “Without your own domain” below); Enter takes it, local means this machine only without TLS. It installs Docker if it is missing, pulls the image, writes /opt/metor/{compose.yml, Caddyfile, .env} (the compose file comes out of the image, so it always matches the version that runs), starts box + Caddy (TLS via Let’s Encrypt as soon as the name resolves to the server) and prints the setup link for your first device, as text and as a QR code. If something already listens on port 80 or 443, the installer leaves its own Caddy off, names the process, and prints the block to add to that proxy (or free the ports and run the installer again - it is idempotent). Open the link, then in the interface click New bot, pick the runtime and sign in to it - Claude Code: open the link, sign in, paste the code the page shows at the end; Codex: device code. Further devices: Settings → Devices → Link a device. A new setup link at any time:

cd /opt/metor && docker compose exec box metor auth link

Unattended (one ssh line)

Every question can be answered up front through environment variables; the script asks only for what is missing and stops with a hint when it has no terminal for a question. Nothing in the installation needs a person at the keyboard:

ssh root@<server> 'METOR_DOMAIN=bots.example.com bash -c "curl -fsSL https://www.metor.com/install.sh | bash"'
VariableMeaning
METOR_DOMAINDomain for the interface; unset = <public-ip>.sslip.io; local (or empty) = this machine only, without Caddy
METOR_INSTALL_DOCKERno = stop instead of installing Docker (with the compose plugin) via get.docker.com when it is missing
METOR_GHCR_USER, METOR_GHCR_TOKENGitHub user + token with read:packages, only for a private image (a mirror via METOR_IMAGE); the official package is public
METOR_DIR, METOR_IMAGEInstall directory (default /opt/metor) and image (default ghcr.io/metor-com/metor-box:latest)

The script ends by printing the setup link; the two manual steps that remain are opening that link on your first device and signing in to a runtime in the interface.

Without your own domain

HTTPS needs a host name that resolves to your server: the session cookie is the key to everything and must never travel over plain HTTP on a public address. You do not have to buy a domain for that; in order of preference:

Whatever name you choose, the sessions are bound to it: a different name (or a new IP with sslip.io) means every device signs in again with a fresh setup link.

B) New server - compose by hand

Copy deploy/compose.yml, deploy/Caddyfile.template and deploy/.env.example to the server, fill in .env, replace @DOMAIN@ in the template with your domain and save it as Caddyfile. Caddy provides HTTPS; the gateway provides device sign-in. Then:

docker compose --profile caddy up -d

Run docker compose exec box metor auth link and open the single-use setup link to sign in your first device. Then connect a runtime from New Bot, or sign in to Claude Code with docker compose exec --user box -e HOME=/home/box -it box /home/box/.local/bin/claude auth login. Runtime login and device pairing are separate.

C) Shared server (your own reverse proxy already runs)

Like B, but without the caddy profile: docker compose up -d - the box then listens only on 127.0.0.1:6010 (the installer does this by itself when it finds port 80 or 443 taken). Forward /bots* from your existing proxy; the sign-in is done by the gateway, the proxy only needs TLS, WebSockets and unbuffered responses. Caddy (the block metor box serve prints):

handle /bots* {
	reverse_proxy 127.0.0.1:6010 {
		flush_interval -1
	}
}

nginx (inside the server block of your HTTPS site; certbot --nginx or your usual way for TLS):

location /bots {
	proxy_pass http://127.0.0.1:6010;
	proxy_http_version 1.1;
	proxy_set_header Host $host;
	proxy_set_header X-Forwarded-Proto $scheme;
	proxy_set_header Upgrade $http_upgrade;
	proxy_set_header Connection "upgrade";
	proxy_buffering off;
	proxy_read_timeout 1h;
}

Then docker compose exec box metor auth link for the first device.

D) Your own Mac (no server)

On an Apple silicon Mac with macOS 26 the bots’ computer runs under Apple’s own container runtime, no Docker needed; on other Macs Colima or Docker Desktop do the same job through the same command.

brew install container            # Apple silicon, macOS 26 – otherwise: brew install colima docker && colima start --cpu 4 --memory 8
brew install metor-com/tap/metor  # the host command (until the tap exists: clone the repository, see the README)
metor setup                       # picks the runtime, gets the image, starts the bots' computer, opens the setup link

metor setup prints the setup link (24 h, single use) and opens it in the browser; paste it into the desktop app instead if you use that. After a reboot, metor box up (or the app’s menu Bots’ computer on this Mac → Start) starts it again; metor box down stops it. The interface listens on http://127.0.0.1:6010/bots/ (METOR_PORT for another port), the three volumes are the same as on a server. Where both Docker and Apple’s runtime are installed, Docker stays the default; METOR_RUNTIME=container metor setup chooses Apple’s once, after that the choice is remembered in ~/.config/metor/runtime. A stopped Docker (Colima or Docker Desktop) is started by the command itself.

E) Development (local, any Docker or Apple container)

See the README: clone the repository, export PATH="$PWD/backend/harness/bin:$PATH", metor box build && metor box up, then sign in from the interface at http://127.0.0.1:6010/bots/ (or docker exec --user box -e HOME=/home/box -it metor-box /home/box/.local/bin/claude auth login). The image is built locally for the machine’s architecture; the ghcr image carries amd64 and arm64. metor version prints the version of the checkout (docker compose exec box metor version prints the version inside the box).

Runtimes

Every bot runs on one runtime; each runtime is signed in once per box and the login persists in its volume.

A runtime whose login expires stops only its own bots; the others keep running. Sign in again and start the bots.

Operations

Options (.env, see deploy/.env.example)

Manage an app-installed server

In the desktop app, open Manage Space → Resources & updates → Manage server…. Verify the SSH fingerprint and sign in as root with a password or private key. This is short-lived access; passwords and passphrases are never saved. Diagnostics show the container state, restarts, Docker OOM flag, free disk, RAM allocation and release. The domain must match the selected Space and the installation must have been created by the app in /opt/metor.

When the app is newer than the Space, Update to … offers its matching release. Confirm Update and restart Space after saving active work and making a server backup. The update retains volumes and RAM settings, installs the release’s matching Compose file, restarts only the box and checks its version. Custom Compose files require a manual update. A failed start restores the previous configuration and attempts to restart the previous image. It does not undo data migrations. Server access closes after the update.

If an interrupted update leaves /opt/metor/.env.before-update or /opt/metor/compose.yml.before-update, another update stops for manual review. Inspect the running image and gateway. Once the result is understood, keep the completed update or restore both saved configuration files and restart the box. Remove the backup files only after confirming the Space is healthy. Do not delete the Docker volumes.

Recovery integration test

On a Mac with Colima and Docker installed, run node scripts/test-server.mjs. It creates a disposable Ubuntu VM, runs recovery, HTTPS, SSH authentication and upgrade checks, then deletes that VM and its test data. It does not activate its Docker context, mount host folders or forward application ports. Allow several minutes for image downloads.

scripts/server-recovery.integration.mjs is an opt-in Linux test for a disposable, empty Docker daemon. It refuses existing containers, volumes and /opt/metor. It runs the actual installer against the released image, injects SIGKILL at download/file/start/ gateway boundaries, retries and checks bot files, history, device sessions, all runtime volumes and the enforced RAM limit. It also checks Caddy HTTPS with a trusted local CA. Set METOR_DISPOSABLE_DOCKER=yes only inside the disposable VM. Never point this test at a real Space or production Docker daemon. Public DNS/ACME/provider firewall acceptance still requires a separate test deployment.

Linux service accounts and existing Spaces

The image prepares volume ownership before starting services. Space administration runs as metor; bot runtimes, browser/desktop tools and local MCP programs run as box. Existing chats, files and runtime login volumes are retained. The container entrypoint needs its default root identity for this offline step; do not override it with user: box or user: metor. Application services drop privileges after preparation. The metor administrative CLI also drops to metor when invoked through container exec.

Back up the volumes before upgrading. An older image cannot necessarily read the new ownership layout; use the pre-upgrade backup when downgrading. A failed ownership check stops startup and retains the data for diagnosis. Do not make the management directory world-writable to bypass it.

This page is metor-com/metor/blob/main/INSTALL.md in the metor repository, rendered as it is.