Linux Local Docker Compose Quick Start Service - Client Tunnel Connection Issues/Confusion

Hi,

I'll preface this by saying I'm new to self-hosting/docker/openziti so I feel like I'm probably missing something obvious. My initial goal is to setup a main self-hosted server with all/most of the services I need in docker containers, which I connect to from other devices remotely using openziti to connect securely.

I'm going to explain all the steps I've taken in case there is something I've done wrong that I've missed but (I think) my issue is with the tunneling client connection (step 10), and so my main question is:

In Linux, how do I correctly setup the tunneler in this case and how can I verify both systemd-resolve and ziti-edge-tunnel are correctly configured/enrolled/running?

For reference I've also modified the "hosts" file adding the edge routers (I think I had to do this to get the ZAC working)

I have been following the quick start guide and I've been struggling with "Create your first service". Following the steps in the guide here's what I've done. The only steps that deviate are 2, 4, 7, 10 and 11.

  1. Created an identity for the HTTP client and assign an attribute "http-clients" by running:
    ziti edge create identity http-client -a 'http-clients' -o http.client.jwt

  2. Skipped "Create an identity for http-server" due to following Docker compose quick-start, so the tunneling server identities already exist

  3. (Exact same as guide) Create an intercept.v1 config with command:
    ziti edge create config http.intercept.v1 intercept.v1 '{"protocols":["tcp"],"addresses":["http.ziti"], "portRanges":[{"low":80, "high":80}]}'

  4. Created a host.v1 config, making sure to set the port to 8000 to match the port assigned by compose and change "${http_server}" to "web.test.blue"
    ziti edge create config http.host.v1 host.v1 '{"protocol":"tcp", "address":"web.test.blue", "port":8000}'

  5. Created a service to associate the two configs created previously into a service.
    ziti edge create service http.svc --configs http.intercept.v1,http.host.v1

  6. Created Dial service with:
    ziti edge create service-policy http.policy.dial Dial --service-roles "@http.svc" --identity-roles '#http-clients'

  7. Create a service-policy to authorize the relevant server id, in this case ziti-private-blue
    ziti edge create service-policy http.policy.bind Bind --service-roles '@http.svc' --identity-roles "@ziti-private-blue"
    (I also tried this with the ID value from list identities but changed the value based on the another thread and the openziti uploaded youtube video I'll link below)

  8. Created edge-router-policy with:
    ziti edge create edge-router-policy "all-routers-all-identities" --edge-router-roles '#all' --identity-roles '#all'

  9. Create all router all service service-edge-router-policy:
    ziti edge create service-edge-router-policy "all-routers-all-services" --edge-router-roles '#all' --service-roles '#all'

  10. Skipped due to docker-compose pre-setup tunneling, not sure if I'm missing something here. (Also small note in the guide here it says to skip to 9 but guessing that's just not been updated to 11)

  11. Start the client-side tunneler using the HTTP client identity. Here's where I think I am getting confused. At first I was trying using android client app by scanning the QR for the http-client jwt in the ZAC. I thought because I was connected on the same local network it would work but I think I was wrong about that. The app said "unavailable" anyway.
          Instead I followed the guide to install the Linux tunneler service (the RPM for fedora in this case) on the server machine. I was trying to use the youtube video here but was struggling to translate the steps with the different environments. I copied the jwt to openziti/etc/identities. This enrolled the identity at first, but then I tried restarting ziti-edge-tunnel and the service failed to start again with the single-use token still in the identities directory, so I removed it and restarted the service. How do I correctly setup the tunneler in this case and how can I verify both systemd-resolve and ziti-edge-tunnel are correctly configured/enrolled/running?

  12. [Fail] I am unable to ping/access http.ziti from anywhere.

Sorry if this is meandering a bit, I've been trying to get it going for a few days now whenever I've had the time so I'm struggling to remember what I've tried and understand how everything works. Would love it if someone could just point me in the right direction. Thanks.

References:

Hi @joey, welcome to the community and to OpenZiti!

Thanks for the detailed post. I've been working on various things to make OpenZiti easier to use, and I'll use this post to help make that doc better. I want to focus on your question:

In Linux, how do I correctly setup the tunneler in this case and how can I verify both systemd-resolve and
ziti-edge-tunnel are correctly configured/enrolled/running?

To do that, let's revisit the whole flow and that doc page. Most critically, let's reconsider using /etc/hosts and instead use something called nip.io. nip.io lets you basically embed an IP address into an FQDN: anything shaped like anything.<ip>.nip.io resolves to <ip>, so ctrl.127.0.0.1.nip.io resolves to 127.0.0.1. It's free, there's nothing to install, and it lets us sidestep all the older local-DNS /etc/hosts guidance you got tangled up in.

So let's start over. First, revert any /etc/hosts edits you made for the controller and routers, we won't need them.

I uploaded a new short video you can watch as well

1. Get the compose file and make an .env

Grab the compose file (saved as compose.yml):

wget -O compose.yml https://gist.githubusercontent.com/dovholuknf/ba3aa83686e60de062a7474c386ff792/raw

Next to it, make a file named .env with these values:

ZIGGY_UID=2171
ZITI_VERSION=2.0.1
ZITI_PWD=change-me-please
ZITI_CTRL_ADVERTISED_ADDRESS=ctrl.127.0.0.1.nip.io
ZITI_CTRL_ADVERTISED_PORT=1280
ZITI_ROUTER_ADVERTISED_ADDRESS=router.127.0.0.1.nip.io
ZITI_ROUTER_PORT=3022

With those nip.io addresses the controller and router advertise names that just resolve to 127.0.0.1, so there's no DNS to set up. If you want other machines to reach it later, use the box's LAN IP instead (e.g. ctrl.192.168.1.50.nip.io) and everything else is the same.

2. Bring the controller online and log in

docker compose pull
docker compose up -d --wait ziti-controller

# use whatever you set for ZITI_PWD
ziti edge login ctrl.127.0.0.1.nip.io:1280 -u admin -p change-me-please -y

Don't proceed until that login works.

3. Verify you can reach the controller by its nip.io name

Again, make sure there's nothing in your /etc/hosts and make sure the nip.io domain name works properly by curling the controller's API. You should just get some json back (that'll indicate success):

curl -sk https://ctrl.127.0.0.1.nip.io:1280
{"data":{"apiVersions":{"edge":{"v1":{"apiBaseUrls":["https://ctrl.127.0.0.1.nip.io:1280/edge/client/v1"],"path":"/edge/client/v1"}},"edge-client":{"v1":{"apiBaseUrls":["https://ctrl.127.0.0.1.nip.io:1280/edge/client/v1"],"path":"/edge/client/v1"}},"edge-management":{"v1":{"apiBaseUrls":["https://ctrl.127.0.0.1.nip.io:1280/edge/management/v1"],"path":"/edge/management/v1"}},"edge-oidc":{"v1":{"apiBaseUrls":["https://ctrl.127.0.0.1.nip.io:1280/oidc"],"path":"/oidc"}},"fabric":{"v1":{"apiBaseUrls":["https://ctrl.127.0.0.1.nip.io:1280"],"path":"/fabric/v1"}}},"buildDate":"2026-07-15T14:57:16Z","capabilities":["OIDC_AUTH","OIDC_AUTH_WITH_CSR","HA_CONTROLLER"],"revision":"ad844fe55b08","runtimeVersion":"go1.26.5","version":"v2.0.1"},"meta":{}}

That's the controller side done.

4. Create the router, service, and policies

Create the router (this makes its identity and saves an enrollment token), then set up the whole service. We do this before the router container starts on purpose, so the router sees the service the moment it connects instead of racing to host something that doesn't exist yet. These are effectively the same commands you ran from the docs, just pointed at the router1 we're about to run:

ziti edge create edge-router router1 --tunneler-enabled --jwt-output-file ./router1.jwt

ziti edge create config http.intercept.v1 intercept.v1 '{"protocols":["tcp"],"addresses":["http.ziti"],"portRanges":[{"low":80,"high":80}]}'
ziti edge create config http.host.v1 host.v1 '{"protocol":"tcp","address":"web.test.blue","port":8000}'
ziti edge create service http.svc --configs http.intercept.v1,http.host.v1 --role-attributes http.services
ziti edge create service-policy http.dial Dial --service-roles '#http.services' --identity-roles '#http-clients'
ziti edge create service-policy http.bind Bind --service-roles '#http.services' --identity-roles '#http-hosts'
ziti edge update identity router1 --role-attributes http-hosts
ziti edge create edge-router-policy all-endpoints --edge-router-roles '#all' --identity-roles '#all'
ziti edge create service-edge-router-policy all-services --edge-router-roles '#all' --service-roles '#all'

5. Start the router (and the web app)

Now bring up the router with its token, plus the demo web server it hosts. The router itself hosts the service (that's --tunneler-enabled and the http-hosts tag) and reaches web.test.blue:8000 over the compose network:

export ZITI_ENROLL_TOKEN="$(< ./router1.jwt)"
docker compose up -d --wait ziti-router web-test-blue

Because the service already exists, the router registers its terminator as it comes up. Confirm the router is online and the terminator is there:

ziti edge list edge-routers   # router1 should be online=true
ziti edge list terminators    # one row for http.svc

Make sure you see a terminator listed. That terminator row is confirming that the host side is live. If there isn't a row returned that is exactly what "unavailable" was telling you before, nothing is hosting the service yet. If it stays empty, run docker compose restart ziti-router and it should register right away. If you don't get a terminator, stop and follow-up here.

6. Add your client identity to the tunneler

Create the client identity:

ziti edge create identity http-client -a http-clients -o http.client.jwt

Make sure ziti-edge-tunnel is installed and running, then hand it the token and verify it is successfully added:

sudo ziti-edge-tunnel add --jwt "$(< http.client.jwt)" --identity http-client
{
  "Success":true,
  "Code":0
}

add enrolls the identity into the running tunneler and keeps it, so there's no leftover .jwt sitting in the identity directory. That leftover single-use token is what was jamming your restarts.

7. Verify DNS and hit the service

ziti-edge-tunnel registers service names with systemd-resolved, so:

resolvectl query http.ziti     # -> 100.64.x.x
curl http://http.ziti/          # -> Hello World

That's it: you dial http.ziti, the overlay carries it to router1, and the router forwards to web.test.blue:8000. Give that a try and let me know how it goes. Thanks again for the writeup, it's helping me clean these docs up.