Deploy zrok on Linux
This guide walks through deploying a self-hosted zrok2 instance on a single Linux server, running the controller, frontend, and metrics bridge. This is the simplest production-ready configuration.
To scale the frontend for higher throughput or availability, see Scaling frontends.
Prerequisites
Before you begin, make sure you have:
-
A Linux server with a public IP
-
A wildcard DNS record like
*.zrok.example.comthat resolves to the server IP -
A wildcard TLS certificate for
*.zrok.example.com(e.g., from Let's Encrypt) -
An OpenZiti controller and router running on your server. OpenZiti provides the secure network backhaul for zrok shares. You can run everything on the same Linux VPS. Follow the OpenZiti Linux deployment guides to install and configure them, then verify the router is online:
ziti edge list edge-routers -
The
zrok2packages installed. Follow the Linux installation guide or run:sudo apt install zrok2 zrok2-controller zrok2-frontend zrok2-metrics-bridge
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.
-
Set the required environment variables:
export ZROK2_DNS_ZONE="zrok.example.com"
export ZROK2_ADMIN_TOKEN="$(head -c24 /dev/urandom | base64 -w0)"
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" -
Run the bootstrap script:
sudo -E /usr/share/zrok/nfpm/zrok2-bootstrap.bash -
Save the value of
ZROK2_ADMIN_TOKEN—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.
-
Install RabbitMQ and PostgreSQL:
sudo apt install rabbitmq-server postgresql -
Add the InfluxData repository and install InfluxDB:
curl -fsSL https://repos.influxdata.com/influxdata-archive.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}" -
Restart RabbitMQ:
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;"
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 "$(head -c24 /dev/urandom | base64 -w0)" \
--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.
# head -c24 /dev/urandom | base64 -w0
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>"admin: Defines privileged administrative credentials. Set the same value in theZROK2_ADMIN_TOKENenvironment variable to runzrok2 admincommands.bridge: Configures the metrics bridge to consume OpenZitifabric.usageevents from a file and publish them to the AMQP queue.metrics: Configures the controller to consume events from the AMQP queue and write them to InfluxDB.dynamic_proxy_controller: Enables the gRPC/AMQP system for dynamic frontends. The identity file referenced here will be created in a later step.
-
Set file ownership and permissions:
sudo chown zrok2-controller:zrok2-controller /etc/zrok2/ctrl.yml
sudo chmod 640 /etc/zrok2/ctrl.yml
- See the reference configuration at
etc/ctrl.ymlfor all configuration options. - See the separate guides on Configure metrics and Configure 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>
For more information on environment variables and advanced configuration, see Use another zrok instance.
Step 4: Bootstrap OpenZiti for zrok
-
Create default traffic policies. These allow all identities to use all edge routers, which is required for shares to work:
ziti edge create edge-router-policy default \
--edge-router-roles '#all' --identity-roles '#all'
ziti edge create service-edge-router-policy default \
--edge-router-roles '#all' --service-roles '#all' -
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
dynamicProxyController 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
Enable and start the zrok2 controller service:
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. Use the Ziti ID of the public
identity created by zrok2 admin bootstrap (shown in its output):
zrok2 admin create frontend --dynamic -- <public-ziti-id> public
This outputs a frontend token (e.g., zEjQqHliYXF6). Save it—you'll need it for the frontend configuration and
namespace mapping.
Step 7: Create the dynamicProxyController
The dynamic proxy controller is a gRPC service that pushes real-time share mapping updates to the frontend over the Ziti
overlay via AMQP. Without it, the frontend can't route named shares (e.g., myapp.zrok.example.com). Only random-token
shares would work with polling.
-
Create the Ziti identity:
zrok2 admin create identity dynamicProxyController -
Log in to Ziti:
ziti edge login <your-ziti-controller>:<port> -y -u admin -p <password> -
Look up the Ziti ID of the identity you just created, then create the service and routing policies:
CONTROLLER_ZID=$(ziti edge list identities 'name="dynamicProxyController"' -j \
| jq -r '.data[0].id')
SERVICE_NAME="dynamicProxyController"
ziti edge create service "$SERVICE_NAME"
ziti edge create serp "${SERVICE_NAME}-serp" \
--edge-router-roles '#all' \
--service-roles "@${SERVICE_NAME}"
ziti edge create sp "${SERVICE_NAME}-bind" Bind \
--identity-roles "@${CONTROLLER_ZID}" \
--service-roles "@${SERVICE_NAME}"
ziti edge create sp "${SERVICE_NAME}-dial" Dial \
--identity-roles "@public" \
--service-roles "@${SERVICE_NAME}" -
Place the
dynamicProxyControlleridentity file where thezrok2-controllerservice user can read it: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 -
Place the
publicfrontend identity file where thezrok2-frontendservice user can read it: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 -
Add the
dynamic_proxy_controllersection to/etc/zrok2/ctrl.yml: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 -
Restart the controller to activate it:
sudo systemctl restart zrok2-controller
When a user creates a named share (zrok2 share public --name-selection public:myapp ...), the controller publishes a
mapping update to the dynamicProxy AMQP exchange. The frontend subscribes to this exchange and immediately starts
routing myapp.zrok.example.com to the share's backend with no polling delay. The dynamicProxyController Ziti service
is the gRPC channel over the Ziti overlay that delivers these mapping updates securely.
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
Replace <frontend-token> with the token from Step 6.
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
host_match: zrok.example.com
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
tls:
cert_path: /etc/letsencrypt/live/zrok.example.com/fullchain.pem
key_path: /etc/letsencrypt/live/zrok.example.com/privkey.pemThe
amqp_subscriberandcontrollersections connect the frontend to the dynamicProxyController gRPC service (Step 7) for real-time mapping updates. Thehost_matchvalue must match the namespace name (Step 8). -
Set file ownership and permissions:
sudo chown zrok2-frontend:zrok2-frontend /etc/zrok2/frontend.yml
sudo chmod 640 /etc/zrok2/frontend.yml -
(If applicable) If the zrok controller terminates TLS directly (i.e., you uncommented the
tls:section inctrl.ymlrather than fronting with Caddy or Traefik), the service users need read access to the certificate files. Let's Encrypt certificates are typically only readable by root.Create a shared group and grant it read access to the certificate files:
sudo groupadd --system zrok2-tls 2>/dev/null || true
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/archiveThen add
SupplementaryGroups=zrok2-tlsto each service's systemd override so the process runs with the group.systemctl editcreates a drop-in override that persists across package upgrades:sudo -E systemctl edit zrok2-controllerAdd:
[Service]
SupplementaryGroups=zrok2-tlsRepeat for the frontend:
sudo -E systemctl edit zrok2-frontendAdd the same
[Service]/SupplementaryGroups=zrok2-tlsblock. Then reload and restart:sudo systemctl daemon-reload
sudo systemctl restart zrok2-controller zrok2-frontend
For a complete reference of all frontend options including OAuth, see the Dynamic proxy frontend migration guide.
Step 11: Start the frontend
-
Enable and start the
zrok2frontend service: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 this 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
networksection of the controller configuration:network:
intervalAgeThreshold: 5s
metricsReportInterval: 5sAnd add to each router's configuration:
metrics:
reportInterval: 5s
intervalAgeThreshold: 5s -
Restart the OpenZiti controller and routers.
For more details, see Configure metrics.
Step 13: Start the metrics bridge
The zrok2-metrics-bridge 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
zrok2-metrics-bridgeuser can readfabric-usage.jsonand write its position pointer alongside it. Add the service user to theziti-controllergroup and grant group write access to the directory:sudo touch /var/lib/ziti-controller/fabric-usage.json
sudo chown ziti-controller:ziti-controller /var/lib/ziti-controller/fabric-usage.json
sudo chmod 0640 /var/lib/ziti-controller/fabric-usage.json
sudo chown ziti-controller:ziti-controller /var/lib/ziti-controller
sudo chmod g+w /var/lib/ziti-controller
sudo usermod -aG ziti-controller zrok2-metrics-bridge -
Start the metrics bridge:
sudo systemctl enable --now zrok2-metrics-bridge -
Verify it's processing events:
sudo journalctl -u zrok2-metrics-bridge -f
Once traffic flows through shares, you should see log output from the controller confirming metrics are being written to InfluxDB.
See Configure limits to enforce bandwidth and resource limits based on these metrics.
Create a user account
With ZROK2_ADMIN_TOKEN and ZROK2_API_ENDPOINT set, create a user account:
zrok2 admin create account <email> <password>
The output is the account token used to enable zrok environments on devices:
Enable your environment
On a client device that can reach your server, register your zrok environment against your instance:
-
Point the CLI at your instance and enable your environment:
zrok2 config set apiEndpoint https://zrok.example.com
zrok2 enable <account-token> -
Set the default namespace for convenience:
zrok2 config set defaultNamespace public -
Verify the environment is active:
zrok2 status
Verify named shares work
Test that the dynamic frontend serves named shares:
-
Pre-create the name in the public namespace:
zrok2 create name mytest -
Create a named share (runs in the foreground; use a separate terminal):
zrok2 share public http://127.0.0.1:8080 --name-selection public:mytest -
From another terminal, verify the frontend routes it:
curl -sf https://mytest.zrok.example.com/
If the share is reachable at mytest.zrok.example.com, the AMQP-backed dynamic frontend is working correctly. The
zrok2 create name step registers the name in the namespace (the v2 equivalent of zrok reserve in v1).
Verify InfluxDB has data
After sending traffic through a share, verify metrics arrived in InfluxDB:
influx query \
'from(bucket: "zrok") |> range(start: -5m) |> count()' \
--org zrok --token "<your-influx-token>" --raw
A successful result contains CSV rows with count values. If no data appears after 90 seconds, check the metrics bridge and RabbitMQ:
sudo systemctl status zrok2-metrics-bridge
sudo rabbitmqctl list_queues
sudo journalctl -u zrok2-metrics-bridge --no-pager -n 50
Congratulations. You have a fully working zrok deployment!
Run as systemd services
The zrok2-controller, zrok2-frontend, and zrok2-metrics-bridge packages install systemd service units for
production deployments. The zrok2-agent package installs a systemd user service for the client side.
zrok controller
Ensure Step 2 is complete, then enable and start the controller service:
sudo systemctl enable --now zrok2-controller
sudo journalctl -u zrok2-controller -f
zrok frontend
Ensure Step 10 is complete, then enable and start the frontend service:
sudo systemctl enable --now zrok2-frontend
sudo journalctl -u zrok2-frontend -f
zrok metrics bridge
Enable and start the metrics bridge service. It reads the bridge section from /etc/zrok2/ctrl.yml:
sudo systemctl enable --now zrok2-metrics-bridge
sudo journalctl -u zrok2-metrics-bridge -f
zrok agent (user service)
Run zrok2 enable first if you haven't already, then enable and start the agent as a systemd user service:
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 Dynamic proxy frontend migration guide.
For metrics troubleshooting (InfluxDB connectivity, AMQP queues, event flow), see Configure metrics.