---
description: Run a Cloudflare Mesh node as a Docker container for Docker Compose, Kubernetes, and CI/CD environments.
title: Run Mesh in Docker / Kubernetes
image: https://developers.cloudflare.com/mesh/guides/run-mesh-in-containers/og.png?v=164eeb2486ebbcab
---

[Skip to content](#main-content)

> Documentation Index  
> Fetch the complete documentation index at: https://developers.cloudflare.com/mesh/llms.txt  
> Use this file to discover all available pages before exploring further.

# Run Mesh in Docker / Kubernetes

Last updated Sep 16, 2026|Copy as Markdown| [View as Markdown](https://developers.cloudflare.com/mesh/guides/run-mesh-in-containers/index.md)| [Agent setup](https://developers.cloudflare.com/agent-setup/)

The [`cloudflare/mesh` ↗︎](https://hub.docker.com/r/cloudflare/mesh) Docker image packages a Cloudflare Mesh node for Linux containers. It runs the Cloudflare One Client's `warp-svc` daemon headlessly in a minimal [Wolfi ↗︎](https://wolfi.dev/)-based runtime.

Use the container image to add Mesh nodes to Docker Compose stacks, Kubernetes clusters, and CI/CD pipelines — without installing packages on the host.

## Supported architectures

The `latest` tag is a multi-platform manifest. Docker automatically selects the appropriate image for the host architecture.

| Architecture | Tag |
| --- | --- |
| Multi-arch | `latest` |
| x86-64 | `latest-amd64` |
| ARM64 | `latest-arm64` |

## Prerequisites

Before starting the container, create a Mesh node and obtain its token.

1. In the Cloudflare dashboard, go to **Networking** > **Mesh**. [Go to **Mesh** ↗](https://dash.cloudflare.com/?to=/:account/mesh)
2. Select **Add participant** > **Add node**.
3. Enter a name for your node (for example, `k8s-gateway` or `docker-agent`).
4. Select **Create node**.
5. Select **Kubernetes**, **Docker Compose**, or **Docker CLI**.
6. Follow the displayed instructions. The dashboard masks the node token but includes it when required by a copied command. Docker Compose configurations and Kubernetes manifests reference a secret instead of containing the token.

With an API token that has either `Cloudflare One Connectors Write` or `Cloudflare One Connector: WARP Write` permission, create a node and retrieve its connector token:

```bash
set -euo pipefail

NODE_RESPONSE=$(
	curl --fail-with-body --silent --show-error \
		"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/warp_connector" \
		--request POST \
		--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN" \
		--header "Content-Type: application/json" \
		--data '{"name":"k8s-gateway"}'
)

jq -e '.success == true and (.result.id | type == "string")' \
	<<< "$NODE_RESPONSE" > /dev/null
NODE_ID=$(jq -r '.result.id' <<< "$NODE_RESPONSE")

TOKEN_RESPONSE=$(
	curl --fail-with-body --silent --show-error \
		"https://api.cloudflare.com/client/v4/accounts/$ACCOUNT_ID/warp_connector/$NODE_ID/token" \
		--header "Authorization: Bearer $CLOUDFLARE_API_TOKEN"
)

MESH_NODE_TOKEN=$(jq -er \
	'select(.success == true) | .result | select(type == "string" and length > 0)' \
	<<< "$TOKEN_RESPONSE")
```

Install `jq`, then set `ACCOUNT_ID` and `CLOUDFLARE_API_TOKEN` before running the commands. The commands stop on an HTTP or API error. Do not continue until they return zero and set `NODE_ID` and `MESH_NODE_TOKEN`. If token retrieval fails after node creation, retry only the token request with the existing `NODE_ID`. Do not rerun the node creation request. Pass `MESH_NODE_TOKEN` to the container.

Note

Mesh nodes can also be managed with Terraform using the [`cloudflare_zero_trust_tunnel_warp_connector` ↗︎](https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/zero_trust_tunnel_warp_connector) resource. To manage node configuration, use [`cloudflare_zero_trust_tunnel_warp_connector_config` ↗︎](https://registry.terraform.io/providers/cloudflare/cloudflare/latest/docs/resources/zero_trust_tunnel_warp_connector_config).

If this is your first Mesh node, configure the [required account settings](https://developers.cloudflare.com/mesh/get-started/#required-account-settings). You can use the dashboard wizard, APIs, or Terraform.

Caution

Do not commit Mesh node tokens to source control. Use environment variables, `.env` files excluded from version control, or a secrets manager.

## Deploy with Docker Compose

Docker Compose is the recommended way to run a Mesh node alongside your application services. Add a `cloudflare-mesh` service to your `compose.yaml`:

```yaml
services:
  cloudflare-mesh:
    image: cloudflare/mesh:latest
    container_name: cloudflare-mesh
    cap_add:
      - NET_ADMIN
      - NET_RAW
    devices:
      - /dev/net/tun:/dev/net/tun
    environment:
      MESH_NODE_TOKEN: ${MESH_NODE_TOKEN}
      SRCNAT_ENABLED: "true"
    sysctls:
      net.ipv4.ip_forward: "1"
      net.ipv6.conf.all.forwarding: "1"
      net.ipv6.conf.default.forwarding: "1"
    volumes:
      - mesh_data:/var/lib/cloudflare-warp
    restart: unless-stopped

volumes:
  mesh_data:
```

Start the stack:

```sh
MESH_NODE_TOKEN="<YOUR-TOKEN>" docker compose up -d
```

Verify the node is connected:

```sh
docker exec cloudflare-mesh warp-cli status
```

## Deploy with Docker CLI

For a standalone container without Compose:

```sh
docker run -d \
  --name cloudflare-mesh \
  --cap-add NET_ADMIN \
  --cap-add NET_RAW \
  --device /dev/net/tun \
  --sysctl net.ipv4.ip_forward=1 \
  --sysctl net.ipv6.conf.all.forwarding=1 \
  --sysctl net.ipv6.conf.default.forwarding=1 \
  -e MESH_NODE_TOKEN="$MESH_NODE_TOKEN" \
  -e SRCNAT_ENABLED=true \
  -v mesh_data:/var/lib/cloudflare-warp \
  --restart unless-stopped \
  cloudflare/mesh:latest
```

## Deploy on Kubernetes

This example creates a one-replica `StatefulSet` with persistent registration state. It requires a Kubernetes cluster that permits `NET_ADMIN`, `NET_RAW`, and `/dev/net/tun` host access (for example, GKE Standard).

### 1. Create the token Secret

```sh
kubectl create secret generic cloudflare-mesh \
  --from-literal=MESH_NODE_TOKEN="$MESH_NODE_TOKEN"
```

### 2. Apply the manifest

Save the following as `cloudflare-mesh.yaml`:

```yaml
apiVersion: v1
kind: Service
metadata:
  name: cloudflare-mesh
spec:
  clusterIP: None
  selector:
    app: cloudflare-mesh
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cloudflare-mesh
spec:
  serviceName: cloudflare-mesh
  replicas: 1
  selector:
    matchLabels:
      app: cloudflare-mesh
  template:
    metadata:
      labels:
        app: cloudflare-mesh
    spec:
      containers:
        - name: mesh
          image: cloudflare/mesh:latest
          env:
            - name: MESH_NODE_TOKEN
              valueFrom:
                secretKeyRef:
                  name: cloudflare-mesh
                  key: MESH_NODE_TOKEN
            - name: SRCNAT_ENABLED
              value: "true"
          securityContext:
            capabilities:
              add:
                - NET_ADMIN
                - NET_RAW
          volumeMounts:
            - name: warp-data
              mountPath: /var/lib/cloudflare-warp
            - name: dev-net-tun
              mountPath: /dev/net/tun
      volumes:
        - name: dev-net-tun
          hostPath:
            path: /dev/net/tun
            type: CharDevice
  volumeClaimTemplates:
    - metadata:
        name: warp-data
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 1Gi
```

### 3. Verify the node

```sh
kubectl apply -f cloudflare-mesh.yaml
kubectl rollout status statefulset/cloudflare-mesh
kubectl exec cloudflare-mesh-0 -- warp-cli status
```

The `PersistentVolumeClaim` preserves the Mesh registration across Pod restarts.

Note

GKE Autopilot is not supported because it blocks the required `/dev/net/tun` `hostPath`.

## Kubernetes sidecar

To connect an application container to Mesh, add the Mesh image as a sidecar in the same Pod. Containers in a Pod share the network namespace, so the Mesh sidecar connects the application to Cloudflare without any application changes.

### 1. Create the token Secret

Create a separate Mesh node and Kubernetes Secret for the sidecar:

```sh
kubectl create secret generic cloudflare-mesh-sidecar \
  --from-literal=MESH_NODE_TOKEN="$MESH_NODE_TOKEN"
```

### 2. Apply the manifest

Save the following as `cloudflare-mesh-sidecar.yaml`:

```yaml
apiVersion: v1
kind: Service
metadata:
  name: cloudflare-mesh-sidecar-headless
spec:
  clusterIP: None
  selector:
    app: cloudflare-mesh-sidecar
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: cloudflare-mesh-sidecar
spec:
  serviceName: cloudflare-mesh-sidecar-headless
  replicas: 1
  selector:
    matchLabels:
      app: cloudflare-mesh-sidecar
  template:
    metadata:
      labels:
        app: cloudflare-mesh-sidecar
    spec:
      containers:
        - name: application
          image: busybox:1.37.0
          command:
            - sh
            - -c
            - |
              echo "Hello from the Kubernetes sidecar example" > /tmp/index.html
              httpd -f -p 8080 -h /tmp
          ports:
            - name: http
              containerPort: 8080
        - name: mesh
          image: cloudflare/mesh:latest
          env:
            - name: MESH_NODE_TOKEN
              valueFrom:
                secretKeyRef:
                  name: cloudflare-mesh-sidecar
                  key: MESH_NODE_TOKEN
            - name: SRCNAT_ENABLED
              value: "true"
          securityContext:
            capabilities:
              add:
                - NET_ADMIN
                - NET_RAW
          volumeMounts:
            - name: warp-data
              mountPath: /var/lib/cloudflare-warp
            - name: dev-net-tun
              mountPath: /dev/net/tun
      volumes:
        - name: dev-net-tun
          hostPath:
            path: /dev/net/tun
            type: CharDevice
  volumeClaimTemplates:
    - metadata:
        name: warp-data
      spec:
        accessModes:
          - ReadWriteOnce
        resources:
          requests:
            storage: 1Gi
---
apiVersion: v1
kind: Service
metadata:
  name: cloudflare-mesh-sidecar
spec:
  selector:
    app: cloudflare-mesh-sidecar
  ports:
    - name: http
      port: 8080
      targetPort: http
```

### 3. Verify the sidecar

```sh
kubectl apply -f cloudflare-mesh-sidecar.yaml
kubectl rollout status statefulset/cloudflare-mesh-sidecar
kubectl exec cloudflare-mesh-sidecar-0 -c mesh -- warp-cli status
```

## Runtime configuration

| Parameter | Description |
| --- | --- |
| `MESH_NODE_TOKEN` | **Required** for initial registration. Create the token under **Networking** > **Mesh** in the [Cloudflare dashboard ↗︎](https://dash.cloudflare.com/?to=/:account/mesh), or via the [API](https://developers.cloudflare.com/api/resources/zero_trust/subresources/tunnels/subresources/warp_connector/methods/create/). |
| `SRCNAT_ENABLED` | Controls [source NAT](#source-nat). Defaults to `true`. Accepts `true`, `false`, `1`, or `0`. |
| `/var/lib/cloudflare-warp` | Stores registration state. Persist this path with a volume to maintain a stable Mesh identity across container recreation. |

<details>

<summary>

Required capabilities and devices

</summary>

| Capability / device | Why it is needed |
| --- | --- |
| <code>NET_ADMIN</code> | Creates and configures the tunnel interface, routing, and nftables rules. |
| <code>NET_RAW</code> | Enables raw-socket operations such as ICMP. Docker normally grants this capability by default, but it is declared explicitly here. |
| <code>/dev/net/tun</code> | Creates the WARP TUN interface. |
| IP-forwarding sysctls | Required when the node forwards traffic for routed subnets. |

</details>

## Source NAT

Source NAT (masquerading) is enabled by default (`SRCNAT_ENABLED=true`). When a Mesh node receives traffic from the Cloudflare edge and forwards it to a destination on the local network, it translates the source IP from the Mesh CGNAT address (`100.96.x.x`) to the node's own local interface IP. This ensures return traffic routes correctly without requiring static routes in your VPC or on-premise network.

Set `SRCNAT_ENABLED=false` only if the attached networks already have return routes to the Mesh IP range (`100.96.0.0/12`). For more details on return traffic routing, refer to [Routes](https://developers.cloudflare.com/mesh/features/routes/#return-traffic-routing).

## High availability on Kubernetes

MASQUE required

This feature requires that the [device profile](https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/device-profiles/) of the Mesh node is configured to use [MASQUE](https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/settings/#device-tunnel-protocol), the default protocol for the Cloudflare One Client. It does not work if the device profile uses WireGuard instead.

For [high availability](https://developers.cloudflare.com/mesh/features/high-availability/) with CIDR routes:

1. Use the same Mesh node token across multiple replicas.
2. Give each Pod its own `PersistentVolumeClaim`.

Cloudflare operates replicas in active-passive mode. If the active replica goes offline, traffic fails over to a standby automatically. A single replica provides no redundancy.

## Hostname routes

Containers support [hostname routing](https://developers.cloudflare.com/mesh/features/routes/#hostname-routes). To resolve Kubernetes Services through a hostname route, make sure the hostname matches the cluster's actual DNS suffix. The default is `cluster.local`, producing Service names like `service.namespace.svc.cluster.local`.

MASQUE required

This feature requires that the [device profile](https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/device-profiles/) of the Mesh node is configured to use [MASQUE](https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/cloudflare-one-client/configure/settings/#device-tunnel-protocol), the default protocol for the Cloudflare One Client. It does not work if the device profile uses WireGuard instead.

## Site-to-site networking

Deploy a separate Mesh node container at each site with a separate node token for each node identity. Each node should advertise its locally reachable subnet as a [CIDR route](https://developers.cloudflare.com/mesh/features/routes/). Configure each site's router or workloads to send traffic for the remote subnet through the local Mesh node.

With `SRCNAT_ENABLED=true`, destinations see the Mesh node's local address. With source NAT disabled, the attached networks require return routes through their Mesh nodes.

## Troubleshooting

### Node registers as a regular Cloudflare One Client device

Confirm that the correct Mesh node token is set in `MESH_NODE_TOKEN`. Existing registration state in the persistent volume takes precedence — remove the volume only when you intentionally want to discard that registration and create a new identity.

### `warp-cli status` remains Connecting

Check the token, device profile, Gateway proxy, Split Tunnel configuration, outbound firewall connectivity, and container logs:

```sh
docker logs cloudflare-mesh
```

### A Kubernetes Service cannot be resolved

Confirm that the [hostname route](https://developers.cloudflare.com/mesh/features/routes/#hostname-routes) matches the cluster's actual DNS suffix. The usual default is `cluster.local`, producing Service names such as `service.namespace.svc.cluster.local`.

### A hostname request arrives but no response returns

Check source NAT and return routing first. Verify `SRCNAT_ENABLED` is set to `true` or that your network has return routes to the Mesh IP range.

### Check node status

```sh
docker exec -it cloudflare-mesh warp-cli status
```

```sh
kubectl exec cloudflare-mesh-0 -- warp-cli status
```

## Next steps

- [**Add routes**](https://developers.cloudflare.com/mesh/features/routes/) — Make subnets behind the containerized node reachable from any device on your Mesh.
- [**Enable high availability**](https://developers.cloudflare.com/mesh/features/high-availability/) — Run multiple replicas for production resilience.
- [**Connect from Workers**](https://developers.cloudflare.com/workers-vpc/examples/connect-to-cloudflare-mesh/) — Use VPC Network bindings to reach private services from Cloudflare Workers.
- [**Tips and best practices**](https://developers.cloudflare.com/mesh/best-practices/) — Cloud VPC configuration, MTU tuning, and running alongside Cloudflare Tunnel.

Was this helpful?

YesNo

## On this page

[![](https://developers.cloudflare.com/_astro/logo.te5VL_aD.svg)Docs](https://developers.cloudflare.com/)

```json
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/mesh/guides/run-mesh-in-containers/#page","headline":"Run Mesh in Docker / Kubernetes","description":"Run a Cloudflare Mesh node as a Docker container for Docker Compose, Kubernetes, and CI/CD environments.","url":"https://developers.cloudflare.com/mesh/guides/run-mesh-in-containers/","inLanguage":"en","image":"https://developers.cloudflare.com/mesh/guides/run-mesh-in-containers/og.png?v=164eeb2486ebbcab","dateModified":"2026-09-16","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"},"keywords":["Private networks","Containers","Docker","Kubernetes"]}
```
