Skip to main content
Star zrok on GitHub Star
Version: 2.0 (Current)

Self-hosting guide for Linux

Before you begin

This will get you up and running with a self-hosted instance of zrok. I'll assume you have the following:

  • a Linux server with a public IP
  • a wildcard DNS record like *.zrok.example.com that resolves to the server IP
  • a wildcard TLS certificate for *.zrok.example.com (e.g., from Let's Encrypt)

OpenZiti

OpenZiti (a.k.a. "Ziti") provides secure network backhaul for zrok public and private shares. You need a Ziti Controller and a Ziti Router. You can run everything on the same Linux VPS.

Follow the OpenZiti Linux deployment guides to install and configure the Ziti Controller and Ziti Router on your server. Once both services are running, verify the router is online:

ziti edge list edge-routers

Install zrok

Follow the Linux installation guide to install the zrok2 package from the repository or manually install the binary for your platform.

sudo apt install zrok2 zrok2-controller zrok2-frontend zrok2-metrics

Automated bootstrap

A bootstrap script is provided that automates the full deployment: PostgreSQL, RabbitMQ, InfluxDB, controller configuration, metrics bridge, dynamic frontend creation, namespace setup, and Ziti service policies. It is idempotent and safe to re-run.

export ZROK2_DNS_ZONE="zrok.example.com"
export ZROK2_ADMIN_TOKEN="$(LC_ALL=C tr -dc _A-Z-a-z-0-9 < /dev/urandom | head -c32)"
export ZITI_API_ENDPOINT="https://127.0.0.1:1280"
export ZITI_ADMIN_PASSWORD="<your-ziti-admin-password>"
export ZROK2_TLS_CERT="/etc/letsencrypt/live/zrok.example.com/fullchain.pem"
export ZROK2_TLS_KEY="/etc/letsencrypt/live/zrok.example.com/privkey.pem"

sudo -E /usr/share/zrok/nfpm/zrok2-bootstrap.bash

Save your ZROK2_ADMIN_TOKEN value — you'll need it for administrative commands.

The bootstrap script uses PostgreSQL by default. To use SQLite3 instead (single-controller deployments only), set ZROK2_STORE_TYPE=sqlite3. Additional optional variables include ZROK2_DB_PASSWORD, ZROK2_INFLUX_TOKEN, and ZROK2_INFLUX_URL — see the script header for the full list.

If you prefer to understand each step or need to customize the setup, continue reading below.

Manual setup

Step 1: Install dependencies

Install the supporting services. The dynamic frontend and metrics systems use RabbitMQ (AMQP), PostgreSQL stores the controller database, and InfluxDB stores usage metrics.

sudo apt install rabbitmq-server postgresql

For InfluxDB, add the InfluxData repository and install:

curl -fsSL https://repos.influxdata.com/influxdata-archive_compat.key \
| sudo gpg --dearmor -o /usr/share/keyrings/influxdata-archive-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/influxdata-archive-keyring.gpg] https://repos.influxdata.com/debian stable main" \
| sudo tee /etc/apt/sources.list.d/influxdata.list
sudo apt update && sudo apt install influxdb2

Enable all three services:

sudo systemctl enable --now rabbitmq-server postgresql influxdb

For security, bind RabbitMQ to localhost only. Add to /etc/rabbitmq/rabbitmq-env.conf:

NODE_IP_ADDRESS=127.0.0.1
SERVER_ADDITIONAL_ERL_ARGS="-kernel inet_dist_use_interface {127,0,0,1}"

Then restart: sudo systemctl restart rabbitmq-server

PostgreSQL setup

Create a database and user for the zrok controller:

sudo -u postgres psql -c "CREATE USER zrok2 WITH PASSWORD '<your-db-password>';"
sudo -u postgres psql -c "CREATE DATABASE zrok2 OWNER zrok2;"
SQLite3 Alternative

For single-controller deployments, you can use SQLite3 instead of PostgreSQL. Replace the store section in ctrl.yml with:

store:
path: zrok.db
type: sqlite3

PostgreSQL is recommended for production and is required for multi-controller deployments and pessimistic locking used by the limits system.

InfluxDB setup

Run the InfluxDB initial setup to create an organization, bucket, and admin token:

influx setup \
--org zrok \
--bucket zrok \
--username admin \
--password "$(LC_ALL=C tr -dc 'A-Za-z0-9' < /dev/urandom | head -c32)" \
--token "<your-influx-token>" \
--retention 0 \
--force

Save the --token value — you'll need it for the controller configuration.

Step 2: Configure the controller

Create /etc/zrok2/ctrl.yml. The key sections are:

v: 4

admin:
# generate from a source of randomness, e.g.
# LC_ALL=C tr -dc _A-Z-a-z-0-9 < /dev/urandom | head -c32
secrets:
- <your-admin-token>

bridge:
source:
type: fileSource
path: /var/lib/ziti-controller/fabric-usage.json
sink:
type: amqpSink
url: amqp://guest:guest@127.0.0.1:5672
queue_name: events

dynamic_proxy_controller:
identity_path: /var/lib/zrok2-controller/.zrok2/identities/dynamicProxyController.json
service_name: dynamicProxyController
amqp_publisher:
url: amqp://guest:guest@127.0.0.1:5672
exchange_name: dynamicProxy

endpoint:
host: 0.0.0.0
port: 18080

# TLS - the zrok controller can terminate TLS directly, or you can front it
# with a reverse proxy
#tls:
# cert_path: /etc/letsencrypt/live/zrok.example.com/fullchain.pem
# key_path: /etc/letsencrypt/live/zrok.example.com/privkey.pem

metrics:
agent:
source:
type: amqpSource
url: amqp://guest:guest@127.0.0.1:5672
queue_name: events
influx:
url: "http://127.0.0.1:8086"
bucket: zrok
org: zrok
token: "<your-influx-token>"

store:
path: "host=127.0.0.1 user=zrok2 password=<your-db-password> dbname=zrok2"
type: "postgres"
enable_locking: true

ziti:
api_endpoint: "https://127.0.0.1:1280"
username: admin
password: "<your-ziti-admin-password>"

Set file ownership:

sudo chown zrok2-controller:zrok2-controller /etc/zrok2/ctrl.yml
sudo chmod 640 /etc/zrok2/ctrl.yml

The admin section defines privileged administrative credentials. Set the same value in the ZROK2_ADMIN_TOKEN environment variable to run zrok2 admin commands.

The bridge section configures the metrics bridge to consume OpenZiti fabric.usage events from a file and publish them to the AMQP queue.

The metrics section configures the controller to consume events from the AMQP queue and write them to InfluxDB.

The dynamic_proxy_controller section enables the gRPC/AMQP system for dynamic frontends. The identity file referenced here will be created in a later step.

note

See the reference configuration at etc/ctrl.yml for all configuration options.

See the separate guides on configuring metrics and configuring limits for details about these specialized areas of service instance configuration.

Step 3: Environment variables

The zrok2 binaries default to using api-v2.zrok.io as the API endpoint. For a self-hosted deployment, set ZROK2_API_ENDPOINT:

export ZROK2_API_ENDPOINT=http://127.0.0.1:18080
export ZROK2_ADMIN_TOKEN=<your-admin-token>

Read more about configuring your self-hosted zrok instance.

Step 4: Bootstrap OpenZiti for zrok

With your OpenZiti network running and your controller config at /etc/zrok2/ctrl.yml, bootstrap the Ziti network:

zrok2 admin bootstrap /etc/zrok2/ctrl.yml

This creates the zrok database, Ziti identities for the controller (ctrl) and frontend (public), and the necessary Ziti policies. Note the frontend identity Ziti ID in the output — you'll use it when creating the frontend.

If you need to re-run bootstrap, add --skip-frontend to avoid re-creating the frontend identity.

Step 5: Start the controller

sudo systemctl enable --now zrok2-controller

Step 6: Create a dynamic frontend

With ZROK2_ADMIN_TOKEN and ZROK2_API_ENDPOINT set, create a dynamic frontend:

zrok2 admin create frontend --dynamic public "https://{token}.zrok.example.com"

This outputs a frontend token (e.g., zEjQqHliYXF6). Save it — you'll need it for the frontend configuration and namespace mapping.

The --dynamic flag enables the v2.0 dynamicProxy mode with namespace-based naming.

Step 7: Create dynamicProxyController identity and Ziti resources

Create a Ziti identity for the dynamicProxyController gRPC service:

zrok2 admin create identity dynamicProxyController

Then create the Ziti service and policies. Log in to Ziti first, then find the Ziti ID of the new identity:

ziti edge login <your-ziti-controller>:<port> -y -u admin -p <password>
# Set the Ziti ID from the identity you just created
CONTROLLER_ZID="<ziti-id-from-above>"
SERVICE_NAME="dynamicProxyController"

# Create the service
ziti edge create service "$SERVICE_NAME"

# Allow edge routers to host the service
ziti edge create serp "${SERVICE_NAME}-serp" \
--edge-router-roles '#all' \
--service-roles "@${SERVICE_NAME}"

# Allow the controller identity to bind (host) the service
ziti edge create sp "${SERVICE_NAME}-bind" Bind \
--identity-roles "@${CONTROLLER_ZID}" \
--service-roles "@${SERVICE_NAME}"

# Allow the frontend identity to dial (connect to) the service
ziti edge create sp "${SERVICE_NAME}-dial" Dial \
--identity-roles "@public" \
--service-roles "@${SERVICE_NAME}"

Copy the identity file to the zrok2-controller service user's directory:

sudo mkdir -p /var/lib/zrok2-controller/.zrok2/identities
sudo cp ~/.zrok2/identities/dynamicProxyController.json \
/var/lib/zrok2-controller/.zrok2/identities/
sudo chown -R zrok2-controller:zrok2-controller /var/lib/zrok2-controller/.zrok2

Also copy the public frontend identity to the zrok2-frontend service user:

sudo mkdir -p /var/lib/zrok2-frontend/.zrok2/identities
sudo cp ~/.zrok2/identities/public.json \
/var/lib/zrok2-frontend/.zrok2/identities/
sudo chown -R zrok2-frontend:zrok2-frontend /var/lib/zrok2-frontend/.zrok2

Restart the controller to activate the dynamicProxyController:

sudo systemctl restart zrok2-controller

Step 8: Create a namespace

Namespaces organize share names (similar to DNS zones). Create a public namespace:

zrok2 admin create namespace --token public --open zrok.example.com

The --open flag allows any account to create names in this namespace. Without it, users need explicit grants.

Step 9: Map namespace to frontend

Link the namespace to the dynamic frontend so shares are served by this frontend:

zrok2 admin create namespace-frontend public <frontend-token> --default

Step 10: Configure the dynamic frontend

Create /etc/zrok2/frontend.yml:

v: 1

frontend_token: <frontend-token-from-step-6>
identity: public
bind_address: 0.0.0.0:443
mapping_refresh_interval: 1m

amqp_subscriber:
url: amqp://guest:guest@127.0.0.1:5672
exchange_name: dynamicProxy

controller:
identity_path: /var/lib/zrok2-frontend/.zrok2/identities/public.json
service_name: dynamicProxyController

host_match: zrok.example.com

tls:
cert_path: /etc/letsencrypt/live/zrok.example.com/fullchain.pem
key_path: /etc/letsencrypt/live/zrok.example.com/privkey.pem

Set file ownership:

sudo chown zrok2-frontend:zrok2-frontend /etc/zrok2/frontend.yml
sudo chmod 640 /etc/zrok2/frontend.yml

If the TLS certificate files are only readable by root (common with Let's Encrypt), grant read access to the service users:

sudo groupadd --system zrok2-tls 2>/dev/null || true
sudo usermod -aG zrok2-tls zrok2-controller
sudo usermod -aG zrok2-tls zrok2-frontend
sudo chgrp -R zrok2-tls /etc/letsencrypt/archive/zrok.example.com/
sudo chmod g+r /etc/letsencrypt/archive/zrok.example.com/*
sudo chmod o+x /etc/letsencrypt /etc/letsencrypt/live /etc/letsencrypt/archive

For a complete reference of all frontend options including OAuth, see the Dynamic Proxy Frontend Guide.

Step 11: Start the frontend

sudo systemctl enable --now zrok2-frontend

Verify it's running:

sudo journalctl -u zrok2-frontend -f

Step 12: Configure OpenZiti metrics events

The zrok metrics pipeline starts at the OpenZiti controller, which emits fabric.usage events. Add the following to your OpenZiti controller configuration:

events:
jsonLogger:
subscriptions:
- type: fabric.usage
version: 3
handler:
type: file
format: json
path: /var/lib/ziti-controller/fabric-usage.json

For responsive metrics, increase the reporting frequency. Add to the network section:

network:
intervalAgeThreshold: 5s
metricsReportInterval: 5s

And add to each router's configuration:

metrics:
reportInterval: 5s
intervalAgeThreshold: 5s

Restart the OpenZiti controller and routers after making these changes.

For more details, see Configuring Metrics.

Step 13: Start the metrics bridge

The zrok2-metrics service runs the metrics bridge as a separate process. It reads the bridge section from /etc/zrok2/ctrl.yml to consume fabric.usage events and publish them to the AMQP queue, where the controller's metrics agent picks them up and writes them to InfluxDB.

Ensure the fabric-usage.json file is readable by the metrics bridge:

sudo touch /var/lib/ziti-controller/fabric-usage.json
sudo chmod o+r /var/lib/ziti-controller/fabric-usage.json

Start the metrics bridge:

sudo systemctl enable --now zrok2-metrics

Verify it's processing events:

sudo journalctl -u zrok2-metrics -f

Once traffic flows through shares, you should see log output from the controller confirming metrics are being written to InfluxDB. See Configuring Limits to enforce bandwidth and resource limits based on these metrics.

Create a user account

With ZROK2_ADMIN_TOKEN and ZROK2_API_ENDPOINT set:

zrok2 admin create account <email> <password>

The output is the account token used to enable zrok environments on devices.

Example output
SuGzRPjVDIcF

Enable your environment

On a client device that can reach your server, configure the API endpoint and enable:

zrok2 config set apiEndpoint https://zrok.example.com
zrok2 enable <account-token>

Set the default namespace for convenience:

zrok2 config set defaultNamespace public
zrok2 status

Congratulations. You have a working zrok environment!

Running as systemd services

The zrok2-controller, zrok2-frontend, and zrok2-metrics packages install systemd service units for production deployments. The zrok2-agent package installs a systemd user service for the client side.

zrok Controller

# ensure /etc/zrok2/ctrl.yml is configured, then:
sudo systemctl enable --now zrok2-controller
sudo journalctl -u zrok2-controller -f

zrok Frontend

# ensure /etc/zrok2/frontend.yml is configured, then:
sudo systemctl enable --now zrok2-frontend
sudo journalctl -u zrok2-frontend -f

zrok Metrics Bridge

# reads the bridge section from /etc/zrok2/ctrl.yml:
sudo systemctl enable --now zrok2-metrics
sudo journalctl -u zrok2-metrics -f

zrok Agent (user service)

The agent runs as a user service. Enable your zrok environment first with zrok2 enable, then:

systemctl --user enable --now zrok2-agent
journalctl --user -u zrok2-agent -f

Troubleshooting

Check service status and recent logs:

sudo systemctl status zrok2-controller
sudo journalctl -u zrok2-controller --since "5 minutes ago"

If a service fails to start, verify the configuration file syntax and that the OpenZiti network is reachable.

For dynamic frontend troubleshooting (AMQP connectivity, gRPC errors, mapping issues), see the Dynamic Proxy Frontend Guide.

For metrics troubleshooting (InfluxDB connectivity, AMQP queues, event flow), see Configuring Metrics.