Server#

gptme-server serves the web UI and the REST API. This page covers running and hosting it; see Web for the interfaces it serves.

Installation#

To use gptme’s server capabilities, install with server extras:

pipx install 'gptme[server]'

Start the server, then open http://localhost:5700:

gptme-server

The server and modern web UI share one origin by default, so no separate frontend process or CORS configuration is required.

For more CLI options, see the CLI reference.

Teaching agents about app previews#

The server proxies authenticated HTTP and WebSocket requests at /preview/{port}/ to 127.0.0.1:{port}. Allowed ports are 1024-65535 except 5700 (the server) and 5900 (raw VNC). This does not start an app or provision noVNC. WebSocket forwarding requires a supported raw-socket WSGI transport. Validate the complete upgrade path, not just raw-socket availability, before advertising WebSocket/HMR or noVNC support.

Operators can advertise that capability through config.runtime.toml and named prompt fragments, without editing user preferences or selecting a different agent profile. Only install the fragment when the deployment actually supports the described preview routes and interface.

The following example assumes the webui and instance server share an origin and the instance is mounted at the origin root:

[prompt.fragments]
live_app_preview = '''
This deployment exposes authenticated live app previews in the webui.
Bind development apps to 127.0.0.1 on ports 1024-65535 except 5700/5900.
They are reachable at /preview/{port}/ relative to the instance server URL.
Configure absolute asset paths and HMR for the public preview base, e.g.
vite --host 127.0.0.1 --port 5173 --strictPort --base=/preview/5173/
WebSockets are supported by this deployment's proxy transport.
If noVNC is running on 6080, use the computer preview panel; its
/preview/6080/vnc.html URL includes the proxied websockify path parameter.
Verify the app is responding before describing it as running.
In the webui, if unsandboxed ipython is available, return this Message as
the final expression (do not print JSON) to register a live_app panel:
```ipython
from gptme.message import Message
Message("system", "App preview ready.", metadata={"panel_hints": [{
    "id": "app-preview-5173",
    "kind": "live_app",
    "title": "App preview",
    "url": "/preview/5173/",
    "status": "running",
    "sandbox": ["allow-scripts", "allow-forms"]
}]})
```
Use a new ID for a new declaration; duplicate IDs do not update status.
Tell the user to open/refresh the Panels sidebar. If Message passthrough
or the webui is unavailable, give the preview link without claiming panel
registration. Never disable sandboxing just to register a panel.
'''

The panel convention is Message.metadata.panel_hints; live_app entries use url (not src) and need status="running" to display the iframe. The unsandboxed IPython tool passes a returned Message through to the conversation log. Its Docker/Wasmtime execution modes do not. Printing a descriptor, writing a Markdown link, or posting metadata to the message-create API does not register a panel. Status is declarative, not automatic health monitoring, and duplicate IDs are first-wins. The panel list refreshes on load or manual refresh.

Important

Operators must substitute the actual browser-visible instance prefix into asset/HMR bases, panel URLs, and user links. For example, a same-origin server mounted at /instances/example needs /instances/example/preview/5173/, not just /preview/5173/. This path is illustrative, not a cloud routing API.

Relative iframe URLs currently resolve against the webui origin, not its configured server base URL. The panel allowlist rejects non-loopback absolute URLs, so a remote absolute URL is not a workaround for a separately hosted webui. Such a topology needs an independently designed resolver; do not weaken the sandbox or allowlist. The existing computer preview component builds noVNC URLs from the server base URL and supplies the full prefixed websockify path.

Deployment startup owns atomic/idempotent runtime-file generation, the public URL/prefix, and knowledge of which tools and preview services are available. It must separately verify authenticated HTTP, WebSockets/HMR, noVNC if provisioned, actual panel rendering, and preservation of user config across restarts. Core config/prompt support alone does not establish cloud deployment readiness or public/shareable previews.

Self-Hosting with Docker Compose#

For a self-contained deployment, a docker-compose.yml is included at the repository root. It builds a lean image (scripts/Dockerfile.selfhost, gptme + the server extra only — no Node/agent tooling) and runs gptme-server with persistent volumes for config and conversation logs.

# Clone the repository
git clone https://github.com/gptme/gptme.git
cd gptme

# Configure: at least one provider key is required
cp .env.example .env
$EDITOR .env

# Build and start the server
docker compose up --build

The server listens on http://localhost:5700.

The modern web UI is bundled at the same origin — just open http://localhost:5700 in a browser. Being same-origin, it needs no CORS setup (you will still need the server token; see below).

To point a separately hosted web UI at this server instead, uncomment the Compose command and set CORS_ORIGIN to that UI’s origin — a cross-origin setup needs it.

Key .env settings:

  • OPENAI_API_KEY / ANTHROPIC_API_KEY / OPENROUTER_API_KEY — at least one is required.

  • GPTME_SERVER_TOKEN — auth token. The server enables auth by default when bound to 0.0.0.0 (as in the container), so set this and configure the web UI with the same value. If left blank, a token is auto-generated at startup — find it with docker compose logs.

  • CORS_ORIGIN — only needed for a separately hosted web UI; uncomment the Compose command and set this to that UI’s origin.

  • GPTME_SERVER_PORT — host port to publish (the container always listens on 5700).

Production Deployment: nginx Reverse Proxy#

The docker-compose setup above publishes the server on a plain HTTP port (5700 by default). For a public-facing deployment you should put it behind a reverse proxy that terminates TLS and forwards requests to the container. The example below uses nginx with a Let’s Encrypt certificate.

Warning

Do not expose the raw 5700 port to the internet. Bind the published port to loopback so only the proxy can reach it. In .env (or docker-compose.yml) set the publish address to 127.0.0.1:

ports:
  - "127.0.0.1:5700:5700"

Also set GPTME_SERVER_TOKEN to a strong value — the proxy handles TLS, but the token is what authenticates each request.

1. Obtain a TLS certificate with certbot (one-time, then auto-renewed):

sudo apt install certbot python3-certbot-nginx
sudo certbot certonly --nginx -d gptme.example.com

Note

The --nginx plugin handles the ACME HTTP challenge through nginx itself, so you do not need to stop nginx first (unlike --standalone, which binds its own listener to port 80 and fails when nginx is already running).

2. nginx site config (/etc/nginx/sites-available/gptme):

server {
    listen 80;
    server_name gptme.example.com;
    # Redirect all HTTP to HTTPS
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl;
    server_name gptme.example.com;

    ssl_certificate     /etc/letsencrypt/live/gptme.example.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/gptme.example.com/privkey.pem;

    # Restrict to TLS 1.2/1.3 with strong ciphers (Mozilla "intermediate" profile)
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
    ssl_prefer_server_ciphers off;

    location / {
        proxy_pass http://127.0.0.1:5700;
        proxy_set_header Host              $host;
        proxy_set_header X-Real-IP         $remote_addr;
        proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;

        # gptme-server streams responses over Server-Sent Events
        # (text/event-stream). Disable proxy buffering and use long
        # read timeouts so streamed tokens are flushed to the client
        # immediately rather than buffered until the response completes.
        proxy_buffering    off;
        proxy_cache        off;
        proxy_read_timeout 3600s;
        proxy_set_header   Connection "";
        proxy_http_version 1.1;
    }
}

3. Enable the site and reload nginx:

sudo ln -s /etc/nginx/sites-available/gptme /etc/nginx/sites-enabled/
sudo nginx -t        # validate config
sudo systemctl reload nginx

The server and bundled UI are now reachable together at https://gptme.example.com. If you instead use a separately hosted UI such from another origin, set CORS_ORIGIN in .env to that UI’s origin and enable the Compose --cors-origin command.

Note

The proxy_buffering off and long proxy_read_timeout settings are the important part: without them nginx buffers the SSE stream and the chat appears to hang until each full response is ready, instead of streaming token by token.

Local-only access. If you only want the server reachable from the host itself (for example, behind a VPN or an SSH tunnel), skip the proxy entirely and keep the default loopback bind — open an SSH tunnel from your client with ssh -L 5700:127.0.0.1:5700 user@host and use http://localhost:5700.

Running as a systemd Service (pipx)#

If you installed gptme directly with pipx rather than Docker, you can run the server as a systemd service so it starts on boot and restarts on failure. A ready-to-edit unit template ships at scripts/gptme-server.service.

The template runs the server as a dedicated gptme user, reads secrets from /etc/gptme/server.env, binds loopback, and applies systemd hardening (ProtectSystem=strict, NoNewPrivileges, etc.). Install it with:

# Dedicated service user + pipx install of the entrypoint
sudo useradd --system --create-home --shell /usr/sbin/nologin gptme
sudo -u gptme pipx install 'gptme[server]'

# Pre-create config/data dirs (required: ProtectHome=read-only only bind-mounts
# paths that already exist; gptme initialises them at import time, which fails
# under the sandbox on a fresh install before any request is served)
sudo -u gptme mkdir -p /home/gptme/.config/gptme \
                       /home/gptme/.local/share/gptme \
                       /home/gptme/.local/state/gptme

# Secrets file (provider keys + GPTME_SERVER_TOKEN), not world-readable
sudo install -d -m 750 -o gptme -g gptme /etc/gptme
sudo install -m 640 -o gptme -g gptme /dev/null /etc/gptme/server.env
sudoedit /etc/gptme/server.env   # add ANTHROPIC_API_KEY=... etc.

# Download and install the unit (adjust User= and the ExecStart path)
sudo curl -fsSL https://raw.githubusercontent.com/gptme/gptme/master/scripts/gptme-server.service \
    -o /etc/systemd/system/gptme-server.service
sudo systemctl daemon-reload
sudo systemctl enable --now gptme-server

Tail logs with journalctl -u gptme-server -f. Because the unit binds 127.0.0.1, pair it with the nginx reverse proxy above to expose it over TLS. Set GPTME_SERVER_TOKEN in the env file to a stable secret so clients have a persistent credential across restarts. This is required for persistent deployments: if omitted the server generates a new random token at each startup, invalidating any client already configured with the previous token.

Security#

This section describes the security model of gptme-server and the threat vectors it is designed (and not designed) to address.

Authentication Model#

gptme-server requires bearer authentication for capability-bearing API routes regardless of bind address. Loopback is a transport boundary, not an identity boundary: another local process must not gain shell and config access merely by reaching 127.0.0.1. A small set of public routes remain unauthenticated: the API root (/api/v2), version info (/api/v2/version), config metadata (/api/v2/config), Prometheus metrics (/api/v0/metrics), and the API documentation at /api/docs/. Set GPTME_SERVER_TOKEN to a fixed value; if unset the server generates one and prints it at startup.

  • GPTME_DISABLE_AUTH disables bearer checks entirely regardless of bind address. Use only behind an authenticated ingress.

Warning

An authorized API client (anyone who can reach the server with a valid token) can execute arbitrary shell commands through the agent. There is no additional sandboxing at the API layer. The security boundary is access to the server, not any individual endpoint.

Host-header validation#

Bearer authentication is enabled for loopback and network binds alike. If an operator explicitly disables authentication with GPTME_DISABLE_AUTH, they can still opt into Host-header validation with gptme-server serve --allowed-hosts gptme.local (comma-separated, or via GPTME_SERVER_ALLOWED_HOSTS).

Threat Model#

The server is hardened against browser-originating attacks that apply to local server access:

1. Cross-Site Request Forgery (CSRF)

A malicious web page cannot send credentialed cross-origin JSON requests to the local server because the browser’s CORS preflight blocks them: the server does not return Access-Control-Allow-Origin headers for arbitrary origins (only for the configured --cors-origin). Plain forms can POST without CORS, but cannot set Content-Type: application/json, so the server rejects them as malformed.

2. DNS Rebinding

A malicious site can re-resolve its hostname to 127.0.0.1 after the page loads and thereby bypass CORS. Bearer authentication still blocks access to capability-bearing routes because the page does not possess the token. If an operator explicitly disables bearer auth, --allowed-hosts can add Host-header validation as defense in depth.

What is NOT in scope:

  • An authorized client redirecting the agent to sensitive files: An authorized API client can already run cat /etc/passwd through the agent. Restricting which directories the agent starts in adds no security boundary — the agent can navigate anywhere from there.

  • Protecting one user from another on a shared server: gptme-server is single-user by design. Multi-user setups must gate access at the network or OS level.

Workspace PATCH Semantics#

Conversations have a workspace — the directory the agent treats as its working directory. The API exposes two operations that touch workspace:

  • PUT /api/v2/conversations/<id> (create): accepts any workspace the client supplies, including paths outside the conversation’s log directory. Rationale: an authorized client can already run arbitrary shell commands; workspace containment at creation adds no security boundary. Cloud pods and the workspace picker both rely on this to set a custom workspace on create.

  • PATCH /api/v2/conversations/<id>/config (update): accepts the round-trip of the already-persisted workspace (the webui settings dialog sends the full config including workspace on every save), but rejects any change that redirects the workspace outside the conversation’s log directory. Rationale: silently redirecting an existing agent’s workspace mid-conversation to an arbitrary path is plausibly a confused-deputy attack vector and is never needed for legitimate updates. If the workspace must change, delete and recreate the conversation.

    Warning

    Deleting a conversation is destructive: its persisted message history is permanently removed. Export or back up conversation logs before deleting if you need to preserve them.

This creates an intentional asymmetry — create-any, re-target-never — to minimize confused-deputy risk without breaking legitimate workflows.

REST API#

gptme-server provides a REST API for programmatic access to gptme functionality. This enables integration with custom applications and automation workflows.

The API endpoints support the core gptme operations including chat interactions, tool execution, and conversation management.

Note

API documentation is available when running the server. Visit the server endpoint /api/docs/ for interactive API documentation based on the OpenAPI spec (served at /api/docs/openapi.json).