Deploying the Zscaler ZPA App Connector with Docker: A Practical Walkthrough

If you’re rolling out Zscaler Private Access (ZPA) in an on-premises environment and want to skip the overhead of a full virtual appliance, running the App Connector as a Docker container is one of the fastest ways to get a connector online. It’s lightweight, easy to automate, and plays nicely with existing container infrastructure if you’re already running workloads that way.

The nice part about this approach is that it’s not picky about the host. As long as the machine can run Docker, it works — a VM in your on-prem hypervisor cluster (VMware, Hyper-V, Proxmox, whatever you’re standardized on), or a bare metal server if you’d rather avoid the hypervisor layer entirely. There’s no dependency on a specific virtualization platform or cloud provider here; the connector just needs outbound internet access and enough CPU/memory headroom to handle the traffic you’re brokering.

In this post, I’ll walk through a working docker run command for deploying the ZPA App Connector, explain what each flag actually does, and cover a few things worth double-checking before you push this into production.

What the App Connector Actually Does

Before touching Docker, it’s worth a quick refresher. The ZPA App Connector is the component that sits inside your private network (data center, VPC, colo, wherever your apps live) and brokers connections outward to the Zscaler cloud. It never accepts inbound connections from the internet — instead, it reaches out to Zscaler and waits for brokered traffic to be handed off to it. That outbound-only model is the whole point of ZPA’s architecture: no inbound firewall rules, no VPN concentrator, no exposed attack surface.

Because the connector needs to inspect and forward traffic on behalf of other machines, it needs a handful of elevated Linux capabilities — which is where most of the flags in the Docker command come from.

Host Prerequisites

Since this is going on-premises rather than in a cloud environment, the checklist is short: a supported Linux distro, a working Docker Engine install, and outbound connectivity to the Zscaler cloud (no inbound ports need to be opened, consistent with ZPA’s outbound-only model). My preference is Debian or Rocky Linux. Beyond that, it doesn’t matter whether the host is a VM or bare metal — Docker abstracts that away. If you’re deploying across several sites, this is also one of the nicer parts of the container approach: the same image and the same docker run command work identically whether you’re standing up a connector in a colo, a branch office server closet, or a VM template you clone repeatedly across your data centers.

I would suggest a minimum of 4 vCPU, ideally 8, and a minimum of 16 GB of RAM, but I would set it to 32 GB preferably. And the rule of thumb is 500 Mbps of traffic with normal encryption, and about 250 Mbps with double-encryption.

The Docker Command

Here’s a typical deployment command for the ZPA Connector container:

docker run -d --init \
--name zpa-connector \
--cap-add cap_net_admin \
--cap-add cap_net_bind_service \
--cap-add cap_net_raw \
--cap-add cap_sys_nice \
--cap-add cap_sys_time \
--cap-add cap_sys_resource \
--restart always \
-e ZPA_PROVISION_KEY="--KEY-HERE--" \
zscaler/zpa-connector:latest.amd64

Let’s break this down piece by piece.

-d --init

-d runs the container in detached mode, which is standard for anything meant to run as a persistent service rather than an interactive session. --init tells Docker to insert a minimal init process (tini under the hood) as PID 1 inside the container. This matters more than it looks — without an init process, signal handling and zombie process reaping can get messy, especially for a long-running network service like this one. It’s a small flag that saves you from a specific category of “why did this container hang on restart” headaches.

--name zpa-connector

Purely for your own sanity. Naming the container makes it easier to reference in scripts, monitoring, and log aggregation instead of hunting for a random hash.

The --cap-add flags

This is the part people usually want explained, since it’s a longer list of capabilities than most containers need:

  • cap_net_admin — Allows network interface configuration, routing table changes, and other admin-level networking operations. The connector needs this to manage its own network stack for traffic brokering.
  • cap_net_bind_service — Lets the process bind to privileged ports (below 1024) without running as root. Useful if the connector needs to listen on standard low-numbered ports for any local services.
  • cap_net_raw — Grants access to raw sockets, which the connector uses for lower-level packet handling and diagnostics.
  • cap_sys_nice — Allows the process to adjust its own scheduling priority, which helps the connector stay responsive under load rather than getting starved by other processes on the host.
  • cap_sys_time — Permits changes to the system clock. ZPA connectors rely on accurate time for TLS handshakes and mutual authentication with the Zscaler cloud, so this exists mostly as a safety net for time drift correction.
  • cap_sys_resource — Lets the process adjust resource limits (like file descriptor counts), which matters for a connector handling many concurrent brokered sessions.

None of these grant full root-equivalent privileges the way --privileged would. That’s a meaningful distinction — you’re scoping the container to exactly the kernel capabilities it needs rather than throwing the door wide open. If you’re running this in an environment with strict container security policies, this list is also your starting point for building an allowlist in your policy engine (Kubernetes PSPs/PSA, AppArmor, SELinux, whatever you’re using).

--restart always

This tells the Docker daemon to restart the container automatically if it exits, crashes, or if the host reboots — provided the Docker service itself is set to start on boot. For something as connectivity-critical as an App Connector, you generally want this. A connector that silently stays down after a host reboot is a bad time to discover your ZPA policy enforcement has a hole in it.

-e ZPA_PROVISION_KEY="--KEY-HERE--"

This is the provisioning key generated from the ZPA Admin Portal, tied to a specific App Connector Group. It’s how the container authenticates itself and registers with your tenant on first boot. A few practical notes here:

  • Provisioning keys are typically time-limited and tied to an enrollment certificate — don’t reuse an old key from a stale deployment guide.
  • Treat this value like a credential, because it functionally is one. Avoid hardcoding it directly into scripts that end up in version control. Pull it from a secrets manager or environment file that’s excluded from your repo.
  • Each key is scoped to a specific App Connector Group, which determines which App Connector policies and configurations the connector inherits. Double check you’re grabbing the key from the correct group, especially if you’re managing multiple environments (dev, staging, prod) in the same tenant.

zscaler/zpa-connector:latest.amd64

The official image, pinned to the amd64 architecture tag. One habit worth adopting for anything beyond a quick test: avoid latest in production. Pin to a specific version tag once you’ve validated it, so an upstream image update doesn’t silently change connector behavior during a routine docker pull. Check Zscaler’s image registry for the current version tags available for your architecture (ARM builds exist too, if you’re deploying on ARM-based hosts).

After the Container Is Running

Once it’s up, a few sanity checks are worth running:

  1. Check container logs with docker logs zpa-connector to confirm the connector successfully registered with the Zscaler cloud. You’re looking for confirmation of a successful connection to the broker, not just a clean container start.
  2. Verify in the ZPA Admin Portal under App Connectors — the new connector should show up as “Connected” within a few minutes.
  3. Confirm resource limits on the host. Docker containers share the host kernel, so if this box is running other workloads, keep an eye on CPU and memory contention, particularly if the connector is brokering meaningful traffic volume.
  4. Set up log shipping early rather than after something breaks. Connector logs are your first stop for troubleshooting brokered connection failures later.

A Few Production Considerations

  • Run more than one connector. A single App Connector is a single point of failure for whatever App Connector Group it belongs to. Zscaler’s own guidance is to deploy connectors in pairs (or more) per group for high availability.
  • Watch host-level updates. Since this container relies on host kernel capabilities, make sure your host OS and Docker engine are patched and stay compatible with the capability set above.
  • Separate provisioning keys per environment. It’s tempting to reuse a key across identical deployments, but keeping dev/staging/prod on separate keys and separate App Connector Groups makes it much easier to reason about blast radius if something goes wrong.
  • Monitor certificate expiry. The enrollment certificate issued during provisioning has its own lifecycle independent of the provisioning key. Don’t set this up and forget it.

Wrapping Up

Running the ZPA App Connector in Docker is a clean, low-friction way to stand up connectivity into ZPA, especially if you’re already comfortable managing containers. The command itself looks intimidating with six capability flags stacked up, but each one maps to a specific, scoped function the connector needs — nothing here is asking for blanket root access.

The bigger things to get right are operational: pin your image version, protect your provisioning key like the credential it is, and don’t run a lone connector in anything you’d call production.

Gregory

Gregory

I'm Gregory from Switzerland, and this is a running log of thoughts, findings, and lessons learned over more than 20 years in IT. With a deep passion for networks and security, I focus on architecture, governance, and emerging technologies. My journey has taken me through complex challenges and continuous learning across various sectors. While this space mainly serves as my personal knowledge base, I hope that sharing these notes might also offer insights or inspiration to others navigating the ever-evolving digital landscape.

You may also like...

Leave a Reply

Your email address will not be published. Required fields are marked *