Self-hostingInstall
Self-hosting

Install

The runner and its panel, on your own machine.

Self-hosted Zeroko is two containers: the runner, which runs the services, and the panel, to manage it and to give agents the skill and the API. Or one: the panel with the runner inside. The images are for amd64 and arm64:

Image
ghcr.io/zeroko-io/runnerThe runner. AGPL.
ghcr.io/zeroko-io/panelThe panel. MIT.
ghcr.io/zeroko-io/panel:<tag>-aioBoth in one container ("all in one").

Tags: X.Y.Z, X.Y and latest for releases; dev follows the main branch. Each panel tag has its all-in-one twin: X.Y.Z-aio, X.Y-aio, latest-aio, dev-aio.

Start

The panel's repository has a docker-compose.yml that starts both, and an .env.example:

sh
cp .env.example .env    # set the token and the password
docker compose up -d
Variable
ZEROKO_TOKENThe runner's root token, 32 characters or more. The panel uses the same one.
ZEROKO_PANEL_PASSWORDThe admin's password, 8 characters or more.
ZEROKO_PANEL_URLThe panel's public address, when its proxy doesn't send X-Forwarded-Host and X-Forwarded-Proto.
PANEL_PORT, GATEWAY_PORTThe ports on the host: 3000 for the panel, 8080 for services.
RUNNER_TAG, PANEL_TAGThe images' tags.

Changing the token or the password signs the admin out.

Or in one container

The all-in-one image is the same panel with the runner inside: the runner's newest release when the image was built. One container, one volume for the runner's data (/data), and only a password to set: the root token is made on the first start and kept in that volume. The panel's repository has a docker-compose-aio.yml for it, and an .env.aio.example:

sh
cp .env.aio.example .env    # set the password
docker compose -f docker-compose-aio.yml up -d
Variable
ZEROKO_PANEL_PASSWORDThe admin's password, 8 characters or more.
ZEROKO_PANEL_URLThe panel's public address, as above.
ZEROKO_TOKENOptional: a root token of your own, 32 characters or more. Without it, one is made and kept in /data.
PANEL_TAGThe tag, without -aio: latest, dev or a version.
PANEL_PORT, GATEWAY_PORTThe ports on the host: 3000 for the panel, 8080 for services.

Or without compose:

sh
docker run -d --name zeroko --restart unless-stopped \
  -p 3000:3000 -p 8080:8080 -v zeroko:/data \
  -e ZEROKO_PANEL_PASSWORD=… ghcr.io/zeroko-io/panel:latest-aio

Inside, the runner and the panel run side by side: the runner as ZEROKO_UID:ZEROKO_GID (1000 by default), the panel as the image's node user. If either stops, the container stops, and Docker's restart policy starts both again. The runner's API (8081) stays inside the container. The root token is never a fixed one, even so: agents reach the runner's API through the panel's /api, where the root token would work too. The rest of this page applies as it is: the proxy, the domain, backups. A newer runner comes with a newer image: pull it to upgrade both.

Put a proxy with TLS in front

Peoplethe panelAgents/api, zk_ tokenVisitorsyour servicesTLS proxyyoursHTTPSHTTPSHTTPSdocker composePanel:3000RunnerGateway:8080API:8081, privatepanel host*.domainroot token
Only your proxy faces the internet. The panel calls the runner's API inside the compose network, with the root token; agents reach that API through the panel's /api.

The panel answers on port 3000 and services on port 8080, both over plain HTTP. Put a reverse proxy with TLS in front of both, whichever you use:

  • one host for the panel, say panel.example.com, to port 3000;
  • a wildcard for the services, *.example.com, to port 8080, with a wildcard certificate or one per service.

The proxy must allow long responses and pass WebSocket upgrades, for realtime connections.

Set the domain

Sign in to the panel and set the domain in System: services are served at <name>.<domain>, and none is until it is set. That restarts the runner in place.

The runner's API (port 8081) stays inside the compose network: agents reach it through the panel, at https://panel.example.com/api, with a token made in Tokens.

Next