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

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.

Single-host deployment

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.com that 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 zrok2 packages 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.

  1. 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"
  2. Run the bootstrap script:

    sudo -E /usr/share/zrok/nfpm/zrok2-bootstrap.bash
  3. 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.

  1. Install RabbitMQ and PostgreSQL:

    sudo apt install rabbitmq-server postgresql
  2. 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
  3. Enable all three services:

    sudo systemctl enable --now rabbitmq-server postgresql influxdb
  4. 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}"
  5. 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;"
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 "$(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

  1. 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 the ZROK2_ADMIN_TOKEN environment variable to run zrok2 admin commands.
    • bridge: Configures the metrics bridge to consume OpenZiti fabric.usage events 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.
  2. Set file ownership and permissions:

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

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

  1. 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'
  2. 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.

  1. Create the Ziti identity:

    zrok2 admin create identity dynamicProxyController
  2. Log in to Ziti:

    ziti edge login <your-ziti-controller>:<port> -y -u admin -p <password>
  3. 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}"
  4. Place the dynamicProxyController identity file where the zrok2-controller service 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
  5. Place the public frontend identity file where the zrok2-frontend service 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
  6. 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
  7. Restart the controller to activate it:

    sudo systemctl restart zrok2-controller
Why is this needed?

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

  1. 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).

  2. Set file ownership and permissions:

    sudo chown zrok2-frontend:zrok2-frontend /etc/zrok2/frontend.yml
    sudo chmod 640 /etc/zrok2/frontend.yml
  3. (If applicable) If the zrok controller terminates TLS directly (i.e., you uncommented the tls: section in ctrl.yml rather 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/archive

    Then add SupplementaryGroups=zrok2-tls to each service's systemd override so the process runs with the group. systemctl edit creates a drop-in override that persists across package upgrades:

    sudo -E systemctl edit zrok2-controller

    Add:

    [Service]
    SupplementaryGroups=zrok2-tls

    Repeat for the frontend:

    sudo -E systemctl edit zrok2-frontend

    Add the same [Service] / SupplementaryGroups=zrok2-tls block. 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

  1. Enable and start the zrok2 frontend service:

    sudo systemctl enable --now zrok2-frontend
  2. 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.

  1. 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
  2. For responsive metrics, increase the reporting frequency. Add to the network section of the controller configuration:

    network:
    intervalAgeThreshold: 5s
    metricsReportInterval: 5s

    And add to each router's configuration:

    metrics:
    reportInterval: 5s
    intervalAgeThreshold: 5s
  3. 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.

  1. Ensure the zrok2-metrics-bridge user can read fabric-usage.json 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
  2. Start the metrics bridge:

    sudo systemctl enable --now zrok2-metrics-bridge
  3. 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:

Example output
SuGzRPjVDIcF

Enable your environment

On a client device that can reach your server, register your zrok environment against your instance:

  1. Point the CLI at your instance and enable your environment:

    zrok2 config set apiEndpoint https://zrok.example.com
    zrok2 enable <account-token>
  2. Set the default namespace for convenience:

    zrok2 config set defaultNamespace public
  3. Verify the environment is active:

    zrok2 status

Verify named shares work

Test that the dynamic frontend serves named shares:

  1. Pre-create the name in the public namespace:

    zrok2 create name mytest
  2. 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
  3. 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.