AgentOS markAgentOS

Run the Gateway in a Container


Run the AgentOS gateway from the published container image on a server, a VPS or a home box, then use it from the desktop app, the Web UI or the CLI on your own machine.

Use this page when the gateway should keep running while your laptop is closed: channels and scheduled jobs stay up, and sessions, memory, provider keys and wallets live on the server. If you only want AgentOS on one Mac, the desktop app runs a gateway for you and you do not need any of this.

The setup has four steps: start the container, give it a provider, choose how your machine reaches it, and point the desktop app at it.

Before You Start

  • A Linux host (amd64 or arm64) with Docker Engine and Compose v2. Check with docker compose version.
  • About 1.1 GB of disk for the image, plus room for your state. The gateway idles around 200 to 250 MiB of memory and uses more during turns.
  • An API key for an LLM provider.
  • SSH access to the host. The first connection below uses nothing else.

1. Start the Gateway

Create a directory for the deployment and download the Compose file:

mkdir -p ~/agentos && cd ~/agentos
curl -fsSLO https://raw.githubusercontent.com/use-agent-os/agent-os/main/compose.yaml

Create a .env next to it with the image and a fresh token:

umask 077
cat > .env <<EOF
AGENTOS_GATEWAY_IMAGE=ghcr.io/use-agent-os/agent-os:latest
AGENTOS_AUTH_TOKEN=$(openssl rand -hex 32)
EOF

Start it and check that it answers:

docker compose up -d
docker compose ps
curl -s http://127.0.0.1:18791/ready

/ready answers with "state":"ready" within a few seconds. docker compose ps reports healthy after the first health check, about 30 seconds in. Clients need the token, so note where it lives: grep AGENTOS_AUTH_TOKEN .env.

Two things are worth knowing about this setup:

  • The gateway is not on the network yet. Inside the container it binds 0.0.0.0, because Docker port publishing needs that, but Compose publishes the port on the host's 127.0.0.1 only. Step 3 decides who else can reach it.
  • The token is required. The gateway refuses to serve a wildcard bind without enforced auth, so compose.yaml turns token auth on and will not start without AGENTOS_AUTH_TOKEN.

Replace latest with a release version (ghcr.io/use-agent-os/agent-os:2026.11.3.post2) to choose when you upgrade. See Upgrade.

2. Configure a Provider

A new gateway has no provider, so agentos doctor reports Active provider is not configured until you set one. Pick whichever of these suits you.

OpenRouter, from .env. Add the key and recreate the container:

echo 'OPENROUTER_API_KEY=YOUR_API_KEY' >> .env
docker compose up -d

compose.yaml forwards only OPENROUTER_API_KEY and BRAVE_SEARCH_API_KEY from .env. Any other name you add there stays out of the container unless you also add it to the environment: block.

Any provider, with the CLI in the container. Store the key at a hidden prompt, which keeps it out of your shell history, then select the provider:

docker compose exec gateway agentos env set OPENAI_API_KEY
docker compose exec gateway agentos configure provider --provider openai --api-key-env OPENAI_API_KEY --model YOUR_MODEL
docker compose exec gateway agentos gateway restart

Run docker compose exec gateway agentos providers list to see the provider names.

From the desktop app or the Web UI, once you have connected in step 4: Settings › Providers works as it does locally. Restart the gateway on its host afterwards if the pane asks for it.

Provider settings and stored keys persist on the volume. Check the result:

docker compose exec gateway agentos doctor

The first line should start with AgentOS Doctor - Ready.

3. Choose How to Reach It

PathWho can reach the gatewayWhat you changeGood for
SSH tunnelanyone with your SSH keynothinggetting started; any host
Tailscale Serveyour tailnet, plus the tokenone command on the hostyour own devices, from anywhere
HTTPS reverse proxythe internet, plus the tokena Compose override and a DNS namea stable address, phones, teammates
LAN portyour network, plus the tokenthe ports: linea trusted home or lab network

Start with the SSH tunnel. It proves the whole chain with no exposure, and you can move to another path later without touching your data.

SSH Tunnel

Run this on your own machine and leave it running:

ssh -N -L 18791:127.0.0.1:18791 user@your-server

The gateway now answers on http://127.0.0.1:18791 on your machine. If that port is taken locally, forward another one (-L 18800:127.0.0.1:18791) and use it in the next step. To survive network drops, use your SSH client's keep-alive options or autossh.

Tailscale Serve

With Tailscale installed on the host, publish the loopback port to your tailnet:

tailscale serve --bg --https=443 http://127.0.0.1:18791

The gateway accepts *.ts.net names without further configuration. Do not use Tailscale Funnel, which publishes the port to the whole internet.

HTTPS Reverse Proxy

Point a DNS name at the host and open ports 80 and 443. Next to compose.yaml, create a compose.override.yaml, which Compose merges in automatically. This one adds Caddy, which obtains and renews the certificate on its own:

services:
  gateway:
    environment:
      AGENTOS_GATEWAY_GATEWAY__PUBLIC_ORIGIN: https://agent.example.com
      AGENTOS_AUTH_TRUSTED_PROXY: 172.30.0.0/24
  caddy:
    image: caddy:2
    command: caddy reverse-proxy --from agent.example.com --to gateway:18791
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - caddy-data:/data
    restart: unless-stopped

networks:
  default:
    ipam:
      config:
        - subnet: 172.30.0.0/24

volumes:
  caddy-data:

Replace agent.example.com in both places, then recreate the stack. The network settings changed, so take it down first. Leave off -v, which would delete your data:

docker compose down
docker compose up -d

Check it from your own machine. The first command should print 200, and the second 401, which proves the token is enforced:

curl -s -o /dev/null -w '%{http_code}\n' https://agent.example.com/healthz
curl -s -o /dev/null -w '%{http_code}\n' https://agent.example.com/api/sessions

What the two settings do:

  • AGENTOS_GATEWAY_GATEWAY__PUBLIC_ORIGIN declares the address clients use. The gateway refuses any hostname it does not know (400) and any browser or app origin it does not know (403). It must match exactly, so a proxy on a port other than 443 needs that port in the origin as well.
  • AGENTOS_AUTH_TRUSTED_PROXY lets the gateway read the real client address from X-Forwarded-For. Without it every client looks like the proxy, so ten wrong tokens from anyone lock everyone out for five minutes. The fixed subnet is what makes this setting possible; choose another one if 172.30.0.0/24 is in use on your host.

Already running a proxy? Send it to 127.0.0.1:18791 or attach it to the same Docker network, and keep the two settings above, with the subnet the proxy connects from. It must forward WebSocket upgrades on /ws and pass the public Host header. A health probe has to send that Host too, or it gets 400 and the proxy marks the gateway down. See Remote access.

LAN Port

To publish the port on your network, edit the ports: line in compose.yaml:

    ports:
      - "192.168.1.20:18791:18791"

Then run docker compose up -d. Traffic, including the token, is plain HTTP, so do this only on a network you trust, and never on a public IP: use the reverse proxy for that. Private addresses work with no other configuration. Docker's published ports also bypass host firewalls such as ufw, so do not count on those to restrict the port.

4. Connect the Desktop App

In the app, open Settings › Gateway › Connection. On a first launch you can get there from the setup screen with Connect to an existing gateway instead, with no engine install.

Set Run mode to External, fill in the values for your path, and press Save:

PathProtocolHostPort
SSH tunnelHTTP127.0.0.118791 (or the local port you forwarded)
Tailscale ServeHTTPSyour-machine.your-tailnet.ts.net443
HTTPS reverse proxyHTTPSagent.example.com443
LAN portHTTP192.168.1.2018791

Auth token is the value of AGENTOS_AUTH_TOKEN from your .env, on every path.

The light in the sidebar footer and the Process row in Settings › Gateway show whether the gateway is reachable. Send a message to confirm that the provider works too.

What changes once the app is on an external gateway:

  • The container owns the data. Sessions, memory, skills and wallets are the gateway's, on the server. Nothing in the app's local ~/.agentos moves over or is deleted; switching back to Managed brings it back.
  • The app no longer runs the gateway. It stops a gateway it had been running for itself. It cannot start or stop this one, and offers Retry connection when the gateway is unreachable.
  • Updates are separate. The app still updates itself. For the gateway, the toolbar shows Gateway outdated when a newer release exists, and you upgrade it on the host.
  • Wallets work. A connection that authenticates with the gateway token from another machine counts as the operator, so you can create, import and unlock wallets and approve orders from the app. See Trading. Anyone holding the token can do the same, so guard it like a wallet password.

More on this mode is in Desktop app.

Web UI and CLI

The same address and token work for the other clients. Open the Web UI at the gateway's address (http://127.0.0.1:18791/control/ through the tunnel) and enter the token under Overview › Gateway connection › Token. For the CLI on your own machine:

export AGENTOS_GATEWAY_TOKEN=YOUR_TOKEN
agentos gateway status --gateway ws://127.0.0.1:18791/ws

Use wss://agent.example.com/ws behind HTTPS.

Day-to-Day Operations

Run these on the host, from the directory that holds compose.yaml.

TaskCommand
Follow the logsdocker compose logs -f gateway
Check healthdocker compose exec gateway agentos doctor
Restart, letting turns finishdocker compose exec gateway agentos gateway restart
Run any CLI commanddocker compose exec gateway agentos ...
Stop, keeping your datadocker compose stop

Changing Settings

.env and compose.yaml are read when the container is created, so changes there need docker compose up -d. docker compose restart keeps the old values, which is a common surprise after rotating a token.

Settings in the gateway's own config file persist on the volume. Name the file explicitly, because agentos config set without --config only prints an export line and changes nothing:

docker compose exec gateway agentos config set gateway.public_origin https://agent.example.com --config /var/lib/agentos/config.toml
docker compose exec gateway agentos gateway restart

Rotating the Token

Put a new value in .env, recreate the container, and update every client, including the desktop app's Auth token:

sed -i "s/^AGENTOS_AUTH_TOKEN=.*/AGENTOS_AUTH_TOKEN=$(openssl rand -hex 32)/" .env
docker compose up -d

Upgrade

Do not run agentos upgrade in the container: the next recreate would throw that away. Replace the image instead. Point AGENTOS_GATEWAY_IMAGE in .env at the new release if you pinned one, then:

docker compose pull
docker compose up -d

State on the volume carries over. Docker gives a stopping container 10 seconds by default, which can cut off turns still running; they are resumed on the next boot where that is safe. To give them the full drain window, add this to compose.override.yaml:

services:
  gateway:
    stop_grace_period: 60s

Take a backup before a large version jump. A newer gateway may migrate the database, so restoring the backup is the reliable way back to an older release.

Back Up and Restore

Everything lives in one named volume mounted at /var/lib/agentos: config, the database, sessions, memory, the workspace, logs and wallets. Stop the gateway so the database is consistent, then archive the volume:

docker compose stop gateway
docker compose run --rm --no-deps -u root -v "$PWD:/backup" --entrypoint sh gateway -c 'umask 077; tar czf /backup/agentos-state.tgz -C /var/lib/agentos .'
docker compose start gateway

The archive holds provider keys and encrypted wallets, so store it accordingly. Keep your wallet password separately. To restore, on a new host or into an empty volume:

docker compose run --rm --no-deps -u root -v "$PWD:/backup:ro" --entrypoint sh gateway -c 'tar xzf /backup/agentos-state.tgz -C /var/lib/agentos'
docker compose up -d

docker compose down keeps the volume. docker compose down -v deletes it, along with any wallets inside that you have not backed up.

What Is in the Image

The image runs as the non-root user agentos (uid 10001), and holds the gateway, the Web UI, Python, curl and the bundled router and embedding models. It does not include git, Node.js, Chromium or bubblewrap, which has consequences:

  • No operating-system sandbox. With bubblewrap missing, the container itself is the boundary around the agent's shell and file tools. Check your posture with docker compose exec gateway agentos sandbox status, and read Tools and sandbox. Do not mount the Docker socket or host directories you would not hand to the agent. A bind-mounted directory must be writable by uid 10001.
  • Git tools and shell commands that need git fail, as do skills and coding-agent harnesses that need Node.js.
  • The browser tool stays hidden. See Browser.

To add what you need, build on the published image and point AGENTOS_GATEWAY_IMAGE at the result:

FROM ghcr.io/use-agent-os/agent-os:latest
USER root
RUN apt-get update && apt-get install -y --no-install-recommends git && rm -rf /var/lib/apt/lists/*
USER agentos
docker build -t agentos-custom:local .

You can also build the base image from a source checkout with docker build -t agentos:local ., after git lfs pull fetches the model files. See Install from source.

Troubleshooting

SymptomCauseFix
Compose stops with an error naming AGENTOS_AUTH_TOKENno .env, or you are in another directoryrun from the directory holding .env, and set the token
Container restarts forever; logs say Refusing to serve: auth.mode='none'a wildcard bind without auth, usually an older or custom Compose fileset AGENTOS_AUTH_MODE: token and AGENTOS_AUTH_TOKEN in environment:
port is already allocatedsomething else holds 18791 on the hostpublish another host port, such as 127.0.0.1:18800:18791, and use it in your clients
401 or UNAUTHORIZEDthe client's token differs from the running onecompare with .env; after editing .env, run docker compose up -d
429 or AUTH_RATE_LIMITEDten wrong tokens in a minute locked the address for five minutesfix the token and wait; behind a proxy, set AGENTOS_AUTH_TRUSTED_PROXY so one client cannot lock out the rest
400, Rejected Host headerthe gateway does not know that hostnamedeclare it with AGENTOS_GATEWAY_GATEWAY__PUBLIC_ORIGIN; make proxy health probes send the public Host
App stays disconnected, WebSocket gets 403the origin is not declared, or its port differsmake the public origin match the app's Protocol, Host and Port exactly
Proxy reaches /healthz, chat never connectsWebSocket upgrades are not forwardedforward Upgrade and Connection on /ws
doctor reports Active provider is not configuredno key reached the gatewayconfigure a provider and restart
A key in .env has no effectcompose.yaml forwards only the names it listsadd it to environment:, or store it with agentos env set in the container
Provider or channel change has not appliedthose settings load at startupdocker compose exec gateway agentos gateway restart

For anything else, start with docker compose logs --tail 100 gateway and docker compose exec gateway agentos doctor, then see Troubleshooting.

Read next: