Deploy and operate

Docker

Run a Hub in the Hubzoid container image with Docker Compose, with sign-in on and its data kept in a mounted Hub folder.

At the end of this guide a Hub runs in a container with sign-in on, its data lives in a Hub folder on the host, and you are signed in as its first administrator. The image runs hubzoid run /hub against the folder you mount, so it behaves like a native install: the same processes, the same .env and the same public port.

Use the image when pip install hubzoid is impractical on the host, when you want the reviewed dependency set, or when your platform runs containers. For the processes and data stores a container runs, see deployment topologies.

What you need

  • Docker with Compose, and a clone of the Hubzoid repository (opens in a new tab) checked out at the release you want (git checkout v<version>), for the Dockerfile and the docker/ Compose files.
  • A model provider key, such as OpenRouter, Anthropic or OpenAI. A container cannot use the claude or codex login on your laptop.

Run a Hub in a container

Build or pull the image

Build the image from the checkout. It installs the reviewed dependency set in requirements.lock, so the image is the release you checked out:

Terminal
docker build -t hubzoid:local .

Or use the published image. Each release is pushed to the GitHub Container Registry for amd64 and arm64, after the release checks pass:

Terminal
docker pull ghcr.io/hubzoid/hubzoid:<version>

Releases are tagged with the version number without the leading v, and with latest. Pin the version tag in production so a restart never changes the release you run.

Make a Hub folder

The Compose file mounts one Hub folder at /hub. If you already have a Hub, use its folder. Otherwise make the starter with the image itself, with no Python needed on the host:

Terminal
mkdir -p ~/my-hub && chmod a+rwX ~/my-hub
docker run --rm -v ~/my-hub:/hub hubzoid:local init /hub

The chmod matters on Linux, because the container runs as its own unprivileged user. You can also clone a ready-to-use Hub instead.

Turn sign-in on in the Hub's .env

The published port is reachable from other machines, so the Compose file turns sign-in on. Before the first start, set a model the container can use and the first administrator in the Hub's .env:

my-hub/.env
MODEL=openrouter/anthropic/claude-haiku-4.5
OPENROUTER_API_KEY=sk-or-...

HUBZOID_ADMIN_EMAIL=sam@example.com
HUBZOID_ADMIN_PASSWORD=<at least 8 characters>
HUBZOID_PUBLIC_URL=https://kestrel.example.com

HUBZOID_PUBLIC_URL is the address people open. It is needed behind a proxy and for Google, Microsoft or OpenID Connect sign-in. Values in the Hub's .env win over the Compose defaults.

Start it with Compose

From the repository checkout, name the Hub folder with HUB_PATH, an absolute path:

Terminal
HUB_PATH=$HOME/my-hub docker compose -f docker/docker-compose.yml up -d

Follow the container's log until the ready line says sign-in is on:

Terminal
docker compose -f docker/docker-compose.yml logs -f hubzoid
Output
✓ Hubzoid is ready: https://kestrel.example.com  (sign-in on)

Sign in and check

Put a reverse proxy with TLS in front of port 3080, exactly as on a single server. Open the address and sign in with the administrator from .env. Administrators must set up two-step sign-in by default, so scan the QR code, enter a code and save the ten recovery codes. Then remove HUBZOID_ADMIN_EMAIL and HUBZOID_ADMIN_PASSWORD from .env.

Run the doctor inside the container:

Terminal
docker compose -f docker/docker-compose.yml exec hubzoid hubzoid doctor /hub

Fix every check marked ✗.

PostgreSQL and the shared browser

The docker/ (opens in a new tab) folder has two more Compose files that add to the base file.

docker/docker-compose.postgres.yml adds a postgres:17 service named db, whose port is not published, and sets the Hub's DATABASE_URL to postgresql+psycopg://hubzoid:${POSTGRES_PASSWORD}@db:5432/hubzoid. The Hub waits for the database health check. Use letters and digits for the password, because it goes into a URL.

Terminal
HUB_PATH=$HOME/my-hub POSTGRES_PASSWORD=<letters and digits> \
  docker compose -f docker/docker-compose.yml -f docker/docker-compose.postgres.yml up -d

Hubzoid's tables and the workflow engine's dbos schema go into the one hubzoid database, stored in the hubzoid-pg volume. Uploads and runtime files stay in the mounted Hub folder. This single DATABASE_URL layout suits one Hub. Deployment topologies explains why.

The image

The image runs hubzoid run /hub as an unprivileged user and publishes one port.

Every property of the image
PropertyValue
Basepython:3.12-slim-trixie (Debian 13, whose SQLite is new enough for the workflow engine)
System packagespostgresql-client, for hubzoid backup of PostgreSQL databases
Python dependenciesrequirements.lock, including the PostgreSQL driver
Userhubzoid, unprivileged
Working directory/hub
Entrypoint and default commandhubzoid, then run /hub
DefaultsPORT=3080, BRIDGE_PORT=8000, HUBZOID_HOST=0.0.0.0
Health checkGET /healthz on PORT every 30 seconds
Published port3080, the edge

Publish only port 3080. The edge there serves the web app, download links, Settings at /settings and the hosted MCP server. The bridge listens on 127.0.0.1 inside the container.

Arguments after the image name replace the default command, for example hubzoid:local run /hub --port 3080. Channels such as Slack turn on with a file in the Hub's channels/ folder, so the default command starts them too. Without Compose, the same Hub runs with docker run -d --restart unless-stopped -p 3080:3080 -v "$HOME/my-hub:/hub" hubzoid:local.

Compose variables and their defaults
Compose variableDefaultWhat it sets
HUB_PATHnone, requiredThe Hub folder mounted at /hub
HUBZOID_IMAGEhubzoid:localThe image. Set ghcr.io/hubzoid/hubzoid:<version> to run a published one.
HUBZOID_PUBLISH3080The host port, or 127.0.0.1:3080 for this machine only
HUBZOID_AUTHtrueSign-in

Where data lives

Everything a Hub writes stays in the folder mounted at /hub, so it survives container replacement and hubzoid backup covers it.

Path in the Hub folderContents
.hubzoid/The SQLite databases (accounts, sessions, conversations, grants, usage and workflow state), the workflow engine's dbos.db, chat files under chats/, the download-link signing key, the deployment key secret.key and scheduled backups under backups/
.inbound/Inbound channel state, including stored webhook events
logs/Hub runtime logs
output/Files the agent made for people

With the PostgreSQL file, the database lives in the named volume hubzoid-pg, and hubzoid backup dumps it into the archive. Keep a copy of the deployment key apart from the data, because backups leave it out. See backup and restore.

Operator commands

Run operator commands inside the container against /hub:

Terminal
docker compose -f docker/docker-compose.yml exec hubzoid hubzoid backup /hub --out /hub/backup.tar.gz
docker compose -f docker/docker-compose.yml exec hubzoid hubzoid admin create priya@example.com /hub
docker compose -f docker/docker-compose.yml exec hubzoid hubzoid schedule status /hub

For liveness probes, the public port answers GET /healthz. GET /healthz/workflows reports workflow health for an uptime monitor. See observability.

To upgrade, back up, stop the container, pull or build the new image and start again. The new version upgrades its tables at its first start. See upgrading.

Container platforms

The image is the entry point for ECS, Kubernetes and other orchestrators. The wiring is yours. Two rules keep a Hub's data safe:

  • One task or pod per Hub. Give each Hub its own service with a desired count of 1 and no horizontal autoscaling. Two copies of one Hub would compete for its workflow engine and SQLite files. Use PostgreSQL when processes run on more than one machine, and rehearse workflow recovery and upgrades.
  • No SQLite on a network filesystem. Keep the .hubzoid databases on block storage such as an EBS volume, not on EFS or another network filesystem, or move Hubzoid's stores to PostgreSQL.

The container listens on PORT (default 3080) and accepts .env values as container environment variables. A value in the Hub's own .env still wins, so leave a setting out of that file to drive it from the platform.

What differs in a container

FeatureIn the image
MODEL=claude-localDoes not work, because the image has no claude CLI. Use a model provider key.
MODEL=codex-localDoes not work in the base image. Build your own image with a supported Codex CLI and private, persistent login storage for the service account. Do not mount an operator's whole home folder.
Shared browser in direct modeThe image has no Node.js, so Hubzoid cannot start the browser itself. Use docker/browser-compose.yml.
Mounted Hub folderMust be writable by the container's user. On Linux, chown -R 999:999 my-hub, or run with --user "$(id -u):$(id -g)".

If something goes wrong

What you seeWhat to do
Set HUB_PATH to your hub folder, an absolute pathCompose needs HUB_PATH. Make a Hub folder as in step 2 and pass its absolute path.
The container stops with Sign-in is off, so listening on 0.0.0.0HUBZOID_AUTH is false in the Hub's .env. Set it to true with a first administrator, or follow the without sign-in tab.
Nobody can sign inNo account exists yet. Set HUBZOID_ADMIN_EMAIL and HUBZOID_ADMIN_PASSWORD, or run hubzoid admin create <email> /hub --owner inside the container.
The container cannot write to /hubThe mounted folder is not writable by the container's user. Fix its ownership as in the table above.
The agent cannot answer with MODEL=claude-local or codex-localThe image has neither CLI. Choose a provider and its key in .env.

Next steps

Choose which cookies Hubzoid can use. You can change this at any time from Cookie settings in the footer. Read the Cookie Policy for details.