Self-hosting guide for Linux
This guide deploys all zrok components — controller, frontend, and metrics bridge — on a single Linux server. This is the simplest production-ready configuration. To scale the frontend for higher throughput or availability, see Scaling zrok frontends.
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.comthat 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-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.
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"
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.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;"
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>"
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.
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
Before running the zrok bootstrap, ensure the default Ziti traffic policies exist. 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
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 cannot 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
Create the Ziti service and routing policies. Log in to Ziti first:
ziti edge login <your-ziti-controller>:<port> -y -u admin -p <password>
Then find the Ziti ID of the new identity and create the service + policies:
# Look up the Ziti ID from the identity you just created
CONTROLLER_ZID=$(ziti edge list identities 'name="dynamicProxyController"' -j \
| jq -r '.data[0].id')
SERVICE_NAME="dynamicProxyController"
# Create the Ziti 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}"
Place the identity files where the systemd service users can read them:
# dynamicProxyController identity → zrok2-controller service user
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
# public frontend identity → 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
Add the dynamic_proxy_controller section 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 — 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.pem
The amqp_subscriber and controller sections connect the frontend to the dynamicProxyController
gRPC service (Step 7) for real-time mapping updates. The host_match value must match the namespace
name (Step 8).
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
Verify named shares work
After creating a user account and enabling an environment (see below), 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 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 — this is the v2
equivalent of zrok reserve in v1.
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-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-bridge user can read the fabric-usage.json file and write its position pointer alongside it. Add the service user to the ziti-controller group 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.
Verify InfluxDB has data
After creating a share and sending some traffic through it, 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
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.
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-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 /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-bridge
sudo journalctl -u zrok2-metrics-bridge -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.