All apps · 0 apps
nordvpn
Docker app from drumandbytes' Repository
Overview
Readme
View on GitHubnordvpn
A minimal, actively-rebuilt container image for the official NordVPN Linux client — installed fresh from repo.nordvpn.com's apt repo on every build, shipped on distroless with no shell, no package manager, and no systemd. Works as a regular VPN tunnel sidecar, a Meshnet node, or both at once — nothing is enabled unless you ask for it.
Why this exists
The existing community images (bubuntux/nordvpn, MattsTechInfo/Meshnet, DPflasterer/nordvpn-meshnet) are all 2-4 years stale. There's nothing complex to the client itself — install the official nordvpn package, run nordvpnd in the foreground instead of under systemd, drive it with the CLI — so this image just does that, and a weekly GitHub Actions build keeps it tracking whatever's current in NordVPN's stable apt channel.
How it's built
Three-stage Dockerfile:
deb-builder(debian:trixie-slim) — installs the realnordvpnpackage plusiptables/iproute2/wireguard-tools/nftables/procpsvia apt. Each of the latter two packages was added after somethingnordvpndshells out to by bare name (PATH lookup) turned out to be missing —nftfor firewall/routing setup duringconnect,sysctlfor interface tuning,psfor norduserd's own process check — invisible until that specific code path was actually exercised.go-builder— compiles a small Go binary (main.go) that replaces the old shellentrypoint.sh/healthcheck.sh. Distroless has no shell at all, so the entrypoint has to be a real binary.- Final stage —
gcr.io/distroless/base-debian13: no apt, no shell, no package manager. Just the specific binaries and shared libraries this needs,COPY'd in explicitly fromdeb-builder(the exact file list came from inspecting the real package withdpkg -L nordvpnand each binary's actual dependency graph withldd— not guessed), plus the compiled entrypoint fromgo-builder.
Net effect: ~40% smaller image, and a meaningfully smaller CVE surface than the full Debian image, since most of Debian's own userland (and its steady stream of base-package CVEs) never makes it into the final image at all.
The Go entrypoint also does one thing the old shell version didn't: since it's PID 1, it reaps any child process nordvpnd spawns and abandons (nordfileshare, norduserd, openvpn) instead of letting them accumulate as zombies, and forwards SIGTERM/SIGINT to nordvpnd properly.
Usage
Regular VPN tunnel (e.g. as a torrent client's network)
docker run -d \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
--device=/dev/net/tun \
-e NORDVPN_TOKEN=<your access token> \
-e NORDVPN_CONNECT=Netherlands \
ghcr.io/drumandbytes/nordvpn:latest
Other containers attach to its network namespace, same as any VPN sidecar:
docker run -it --net=container:<this container's name> -d your/other-image
As a gluetun-style VPN sidecar
Kill switch on, cluster traffic and probe ports allowed through, connected before the other containers start using the network:
docker run -d \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
--device=/dev/net/tun \
-e NORDVPN_TOKEN=<your access token> \
-e NORDVPN_ALLOWLIST="subnet 10.244.0.0/16; subnet 10.96.0.0/12; port 8080" \
-e NORDVPN_SET="killswitch on; technology nordlynx" \
-e NORDVPN_CONNECT=Netherlands \
ghcr.io/drumandbytes/nordvpn:latest
Don't combine this with lan-discovery on: the client then silently drops private (10/8, 172.16/12, 192.168/16) subnets from the allowlist, which in a pod means the cluster CIDRs. Both variables only ever reach nordvpn set and nordvpn allowlist add/remove all. The image stays shell-less, and on a restart with a persisted /var/lib/nordvpn the client's "already set / already on the allowlist" replies are treated as success.
Meshnet node
docker run -d \
--cap-add=NET_ADMIN --cap-add=NET_RAW \
--device=/dev/net/tun \
-e NORDVPN_TOKEN=<your access token> \
-e NORDVPN_MESHNET=on \
-e NORDVPN_NICKNAME=my-device \
ghcr.io/drumandbytes/nordvpn:latest
Generate an access token: NordVPN account → Meshnet (or VPN) → Manual setup → Access token.
Environment variables
| Variable | Description |
|---|---|
NORDVPN_TOKEN |
Access token used to log in. Required unless a session is already persisted in a mounted /var/lib/nordvpn. |
NORDVPN_CONNECT |
Connects to a VPN server. Empty value = recommended server; otherwise a country/city/server/group, anything nordvpn connect itself accepts. Unset by default (no VPN tunnel). Flags pass through too: --group P2P Netherlands. |
NORDVPN_MESHNET |
on to enable Meshnet. Unset/anything else = off. |
NORDVPN_NICKNAME |
Sets this device's Meshnet nickname (nordvpn meshnet set nickname). Only applies when NORDVPN_MESHNET=on. Without one, every restart can show up as a new device unless state is persisted. |
NORDVPN_FIREWALL |
on or off. Unset by default — leaves the client's own default behavior alone. If you're using NORDVPN_MESHNET=on without NORDVPN_CONNECT (Meshnet only, no VPN exit server), you probably want this set to off explicitly, since the killswitch otherwise blocks the container's normal non-tunnel traffic. |
NORDVPN_LOG_LEVEL |
debug, info (default), warn, or error. nordvpnd defaults to its most verbose (debug) when it can't find a log-level file to read, which is always true on a fresh container — this just writes one before starting the daemon. |
NORDVPN_SET |
;-separated settings, each passed to nordvpn set: e.g. killswitch on; technology nordlynx; post-quantum on. Anything nordvpn set accepts works. Applied after login and before NORDVPN_CONNECT. |
NORDVPN_ALLOWLIST |
;-separated entries, each passed to nordvpn allowlist add: e.g. subnet 10.244.0.0/16; port 8080. Applied before NORDVPN_SET, so a kill switch never cuts off what you allowlist. Replaces the whole allowlist each start (set it empty to clear); unset leaves it alone. |
Persisting device/session identity
Mount a volume at /var/lib/nordvpn to keep the same login and Meshnet device (and any peer permissions granted to it) across container restarts.
Kubernetes
containers:
- name: nordvpn
image: ghcr.io/drumandbytes/nordvpn:latest
securityContext:
capabilities:
add: ["NET_ADMIN", "NET_RAW"]
env:
- name: NORDVPN_TOKEN
valueFrom:
secretKeyRef:
name: nordvpn-token
key: token
- name: NORDVPN_MESHNET
value: "on"
- name: NORDVPN_FIREWALL
value: "off"
volumeMounts:
- name: dev-net-tun
mountPath: /dev/net/tun
- name: nordvpn-state
mountPath: /var/lib/nordvpn
livenessProbe:
exec:
command: ["/entrypoint", "healthcheck"]
initialDelaySeconds: 30
periodSeconds: 30
volumes:
- name: dev-net-tun
hostPath:
path: /dev/net/tun
- name: nordvpn-state
persistentVolumeClaim:
claimName: nordvpn-state
Put other containers in the same Pod spec to share this one's network — containers in a Pod share a network namespace natively, no extra config needed.
The host kernel needs iptables/netfilter modules available — the NordVPN client uses iptables internally to route and firewall its traffic.
Meshnet specifically needs more than NET_ADMIN/NET_RAW. Its routing setup writes net.ipv4.conf.all.rp_filter, which stays read-only under Kubernetes/Docker regardless of those capabilities — confirmed directly, including that Docker's own --sysctl flag only sets the initial value, not ongoing write access. Nothing short of securityContext.privileged: true unlocks it. A plain VPN tunnel (NORDVPN_CONNECT without NORDVPN_MESHNET) doesn't hit this and works fine with just the two capabilities above.
Extracting the WireGuard key (e.g. for gluetun)
NordLynx is WireGuard underneath, so once connected the private key and assigned address are readable with wg (included in this image):
docker run -d --name nordvpn-keygen \
--cap-add=NET_ADMIN --cap-add=NET_RAW --device=/dev/net/tun \
-e NORDVPN_TOKEN=<your access token> -e NORDVPN_CONNECT= \
ghcr.io/drumandbytes/nordvpn:latest
docker exec nordvpn-keygen wg show nordlynx private-key
docker exec nordvpn-keygen ip -4 addr show nordlynx # the /32 address to pair with it
docker rm -f nordvpn-keygen
Building locally
docker build -t nordvpn .
Verifying the image
Every published image carries a signed SLSA build provenance attestation (generated by GitHub Actions, stored alongside the image in GHCR):
gh attestation verify oci://ghcr.io/drumandbytes/nordvpn:latest --owner drumandbytes
This confirms the image was built from this repo by the Build workflow, and
not pushed from anywhere else.
How it was made
Built with the help of an AI coding assistant (Claude). I review and test what gets published.
License
MIT — see LICENSE. Not affiliated with or endorsed by Nord Security.
Install Nordvpn on Unraid in a few clicks.
Find Nordvpn in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.
Categories
Related apps
Explore more like this
Explore allDetails
ghcr.io/drumandbytes/nordvpn:latestRuntime arguments
- Network
bridge- Privileged
- false
- Extra Params
--cap-add=NET_ADMIN --cap-add=NET_RAW --device=/dev/net/tun
Template configuration
NordVPN account → Manual setup → Access token.
- Target
- NORDVPN_TOKEN
Country/city/server/group, anything `nordvpn connect` accepts. Empty = recommended server.
- Target
- NORDVPN_CONNECT
; separated, each passed to `nordvpn set`.
- Target
- NORDVPN_SET
- Default
- killswitch on; technology nordlynx
- Value
- killswitch on; technology nordlynx
; separated, each passed to `nordvpn allowlist add`. Replaces the allowlist each start.
- Target
- NORDVPN_ALLOWLIST
- Default
- subnet 192.168.0.0/16
- Value
- subnet 192.168.0.0/16
on to enable Meshnet.
- Target
- NORDVPN_MESHNET
Login and Meshnet identity across restarts.
- Target
- /var/lib/nordvpn
- Default
- /mnt/user/appdata/nordvpn
- Value
- /mnt/user/appdata/nordvpn