Self-hosted service
cmj2002/warp-docker avatar
cmj2002/warp-docker

warp-docker: Cloudflare WARP behind a SOCKS5 proxy in Docker

Run Cloudflare WARP in Docker.

1,053 stars251 forksShellGPL-3.0

At a glance

What is it?
warp-docker packages the official Cloudflare WARP client together with the GOST proxy into a single container that exposes HTTP and SOCKS5 on port 1080. It needs NET_ADMIN and a device cgroup rule, and it lets you pin both the WARP and GOST versions in the image tag.
Who is it for?
Adopt warp-docker if you want a WARP tunnel as a local SOCKS5 proxy that composes with the rest of your container stack, on Docker or Podman, without installing the WARP client on the host.
Can I use it commercially?
Yes, with conditions. GPL-3.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 83 days ago.
What is it written in?
Mainly Shell, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Two programs in one container

warp-docker runs the official Cloudflare WARP client in Docker, and the interesting part is that it also runs a proxy in the same container. The WARP client alone is a system daemon that creates a tunnel; GOST is the piece that exposes that tunnel as a proxy you can point other software at.

The default GOST_ARGS value is -L :1080, which means it listens on port 1080 inside the container and speaks both HTTP and SOCKS5 there. The result is a container you can treat as a proxy endpoint on localhost, which is a different shape from a host-wide VPN and a much easier one to compose with a docker-compose stack.

The repository is small by design. The top level holds a Dockerfile, a docker-compose.yml, an entrypoint.sh, a healthcheck/ directory, a docs/ directory and the GitHub Actions workflows, and the primary language is Shell. The image is published on Docker Hub as caomingjun/warp, and the README carries badges for the pull count and for the WARP and GOST versions inside the latest image.

One caveat comes first, in the project's own note: the image cannot guarantee that the GOST and WARP client it contains are the latest versions, and if that matters you are pointed at building your own image. That is a fair characterisation of any wrapper that pins a vendor binary in a layer.

The compose file, and the privileges it demands

Using the project means writing a compose file and running docker-compose up -d. The documented file starts like this:

yaml
version: "3"

services:
  warp:
    image: caomingjun/warp
    container_name: warp
    restart: always
    # add removed rule back (https://github.com/opencontainers/runc/pull/3468)
    device_cgroup_rules:
      - 'c 10:200 rwm'
    ports:
      - "1080:1080"
    environment:
      - WARP_SLEEP=2

The rest of the file continues with the capabilities, the sysctls and the volume, and those three blocks are where the real story is.

cap_add requests MKNOD, AUDIT_WRITE and NET_ADMIN. The comment in the file explains that Docker already has the first two and that they are there for Podman users, while NET_ADMIN is described as the additional capability WARP needs in both runtimes. In other words, the container is given permission to change network configuration, which is exactly what a daemon that builds a tunnel requires, and also exactly what you would rather not hand to a container that pulls binaries from a third party package repository.

sysctls sets net.ipv6.conf.all.disable_ipv6=0 and net.ipv4.conf.all.src_valid_mark=1, with a commented block for net.ipv4.ip_forward and the IPv6 forwarding and accept_ra settings used when NAT is enabled. The src_valid_mark setting exists because the tunnel relies on policy routing marks, and the IPv6 switch is there because the WARP client refuses to work with IPv6 disabled.

volumes maps ./data to /var/lib/cloudflare-warp, and that is the piece that makes the container survive a restart, because it holds the client registration.

The device_cgroup_rules line with the character device 10:200 is a workaround for a rule that was removed from the default container runtime configuration, and it is the kind of detail that tells you this project has been run on a lot of different hosts.

Checking the tunnel with the trace endpoint

There is a one-line way to know whether the container is actually working, and the project gives it to you:

bash
curl --socks5-hostname 127.0.0.1:1080 https://cloudflare.com/cdn-cgi/trace

The interpretation is spelled out. If the output contains warp=on or warp=plus, the container is working properly. If the output contains warp=off, the container failed to connect to the WARP service.

Two details in that command are worth copying rather than improvising. The socks5-hostname option tells curl to send the hostname to the proxy rather than resolving it locally, which is what you want when the point of the proxy is that the resolution happens on the other side of the tunnel. And the endpoint is Cloudflare's own trace page, which reports the connection properties of the request, so it tells you both that the tunnel is up and what the exit looks like.

The failure modes are equally worth knowing, because WARP in a container is sensitive to timing and to the host's networking. The WARP_SLEEP setting exists for exactly this reason. It is the number of seconds to wait for the WARP daemon to start, and the default is 2. If it is too short, the daemon may not be ready before the proxy is used, and the proxy will not work properly. If it is too long, the container takes that much longer to start. On a slow server you raise it.

There is also a documented host connectivity problem, with a page in docs/ called host-connectivity.md, and the environment variable BETA_FIX_HOST_CONNECTIVITY which adds host connectivity checks to the healthchecks and fixes the problem when needed.

The environment variables that change behaviour

Beyond the sleep timer, four variables are worth understanding before you deploy.

WARP_LICENSE_KEY takes the licence key of the WARP client and is optional. If you subscribe to WARP+ you fill it in, and if you do not, you ignore it. The documentation adds a practical instruction: if you modify the key, delete the ./data directory so the client can detect and register again. That is the same volume that persists the registration, so a stale registration is the thing being cleared.

GOST_ARGS passes arguments to GOST, and the default of -L :1080 gives you an HTTP and SOCKS5 proxy on one port. If you want UDP support or the advanced features of other protocols you change it, and the documentation points at the GOST documentation for the protocol options. If you change the port inside the container, you need to change the port mapping in the compose file to match, which is an easy mistake to make.

REGISTER_WHEN_MDM_EXISTS registers a consumer account, WARP or WARP+, even when an mdm.xml file is present. The documentation says you usually do not need it, because mdm.xml is normally used for Zero Trust, but some users want to adjust advanced settings in that file while still using a consumer account.

WARP_ENABLE_NAT makes the container work in warp mode with NAT turned on, so you can route layer 3 traffic through it, and there is a separate document at docs/nat-gateway.md for that setup. The compose file carries the matching commented sysctls for ip_forward and the IPv6 forwarding settings.

The data directory is the other piece of the configuration story. The host volume ./data persists the WARP client data, and you can move it or use a different volume type. The advanced configurations live in docs/README.md.

Version tags, and the experimental slim images

The image tag encodes exactly what is inside it, in the form {WARP_VERSION}-{GOST_VERSION}. The example given is 2023.10.120-2.11.5, meaning the WARP client version is 2023.10.120 and the GOST version is 2.11.5. If you want a different combination, you specify the tag in your compose file, and there is a latest tag for the newest build.

That scheme is what makes the project usable in a reproducible deployment, and it comes with two conditions the documentation states plainly. Not all version combinations are available, so you check the list of tags on Docker Hub before you pick one. And if the version you want is not there, you build your own image. You can also pin a specific commit with the tag {WARP_VERSION}-{GOST_VERSION}-{COMMIT_SHA}, though not every commit has an image built.

The slim variants are the newer addition and they are marked experimental. The repository publishes an additional set of images based on debian:bookworm-slim, tagged with a slim- prefix, in the form slim-latest, slim-{WARP_VERSION}-{GOST_VERSION} and slim-{WARP_VERSION}-{GOST_VERSION}-{COMMIT_SHA}. The reason is a smaller base image, which reduces image size and memory usage. The documentation is careful about the status: tests are still ongoing to ensure that all functionalities work as expected, and you are told to use them at your own risk, with issues to be reported in issue 67.

The Dockerfile itself is where the version pinning becomes concrete. It takes build arguments for the base image, the WARP version, the GOST version, the commit SHA and the target platform, labels the image with the WARP, GOST and commit versions so the badge can read them, then installs the cloudflare-warp package from Cloudflare's own apt repository and unpacks the GOST archive, choosing between two filename conventions depending on whether the GOST version is 2.12.0 or newer.

Building the image yourself

If you want the newest WARP client, or a GOST version that is not published, the documented path is to build the image with GitHub Actions, and the steps are spelled out.

You fork the repository. Then you create variables and secrets in the repository settings: a variable REGISTRY, for example docker.io for Docker Hub, a variable IMAGE_NAME, for example caomingjun/warp, a variable DOCKER_USERNAME, for example caomingjun, and a secret DOCKER_PASSWORD, for which you generate a token in Docker Hub and fill in the token. After that you manually trigger the workflow named Build and push image in the Actions tab.

The result is an image built with the latest version of the WARP client and GOST, pushed to the registry you named. You can also give the GOST version as an input to the workflow. Building an image with a custom WARP client version is not supported yet, which is a real constraint if you need to pin the WARP client to a specific build.

If you want to build locally instead, the documentation points you at the workflow file .github/workflows/build-publish.yml as the reference for how the image is assembled.

The licence is GPL-3.0. The repository is not archived, the last push was on 2026-08-01, and there are no GitHub releases, so the image tags on Docker Hub are the version history that actually matters to a user. The image is also used with Podman, which is why the capability list includes MKNOD and AUDIT_WRITE even though Docker already has them.

Editorial conclusion

Adopt warp-docker if you want a WARP tunnel as a local SOCKS5 proxy that composes with the rest of your container stack, on Docker or Podman, without installing the WARP client on the host. Do not adopt it if you need a full VPN that routes every packet of the host, because the default configuration is a proxy process on one port, and treat the capabilities it asks for as a real cost, since NET_ADMIN and the device cgroup rule give the container privileges that the WARP daemon needs. Before you start, read the note about image freshness and build your own image if the bundled versions matter to you, keep the ./data volume so your registration survives restarts, and check the trace endpoint after every start to confirm the output says warp=on rather than warp=off.

Frequently asked questions

How do I run Cloudflare WARP in Docker with warp-docker?

Write the compose file with the caomingjun/warp image, map port 1080, add the NET_ADMIN capability and the sysctls, and mount ./data to /var/lib/cloudflare-warp, then run docker-compose up -d. The default GOST_ARGS of -L :1080 serves both HTTP and SOCKS5 on that port.

How do I check that the WARP container is actually connected?

Run curl --socks5-hostname 127.0.0.1:1080 https://cloudflare.com/cdn-cgi/trace. The output containing warp=on or warp=plus means the container is working properly, and warp=off means it failed to connect to the WARP service.

What does the WARP_SLEEP setting do?

It is the number of seconds to wait for the WARP daemon to start, and the default is 2. If it is too short the daemon may not be ready before the proxy is used, and if it is too long the container takes longer to start, so a slow server needs a larger value.

How do I choose a specific version of the image?

Image tags have the form {WARP_VERSION}-{GOST_VERSION}, for example 2023.10.120-2.11.5, and you can also pin a commit with {WARP_VERSION}-{GOST_VERSION}-{COMMIT_SHA}. Not every combination is published, so check the tag list on Docker Hub and build your own image if the version you need is missing.

Are the slim images in warp-docker safe to use?

They are published as experimental. The slim variants are based on debian:bookworm-slim to reduce image size and memory usage, and the documentation says tests are still ongoing to confirm that all functionality works as expected, with issues to be reported in issue 67.

Official sources

  1. cmj2002/warp-docker on GitHub
  2. Issues
  3. License: GPL-3.0
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/cmj2002-warp-docker.svg)](https://hysenlabs.com/projects/cmj2002-warp-docker)

Was this useful?