nordvpn

nordvpn

Docker app from drumandbytes' Repository

Overview

The official NordVPN Linux client on distroless: no shell, rebuilt when upstream changes. Use it as a VPN network for other containers (Network Type: Container) with kill switch and allowlist set from env, or as a Meshnet node. Needs NET_ADMIN, NET_RAW and /dev/net/tun (set in Extra Parameters).

nordvpn

Build License: MIT

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:

  1. deb-builder (debian:trixie-slim) — installs the real nordvpn package plus iptables/iproute2/wireguard-tools/nftables/procps via apt. Each of the latter two packages was added after something nordvpnd shells out to by bare name (PATH lookup) turned out to be missing — nft for firewall/routing setup during connect, sysctl for interface tuning, ps for norduserd's own process check — invisible until that specific code path was actually exercised.
  2. go-builder — compiles a small Go binary (main.go) that replaces the old shell entrypoint.sh/healthcheck.sh. Distroless has no shell at all, so the entrypoint has to be a real binary.
  3. 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 from deb-builder (the exact file list came from inspecting the real package with dpkg -L nordvpn and each binary's actual dependency graph with ldd — not guessed), plus the compiled entrypoint from go-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.

Open the Apps tab on your Unraid server Search Community Apps for Nordvpn Review the template variables and paths Click Install

Categories

Related apps

Explore more like this

Explore all

Details

Repository
ghcr.io/drumandbytes/nordvpn:latest
Last Updated2026-09-28
First Seen2026-09-28

Runtime arguments

Network
bridge
Privileged
false
Extra Params
--cap-add=NET_ADMIN --cap-add=NET_RAW --device=/dev/net/tun

Template configuration

Access tokenVariable

NordVPN account → Manual setup → Access token.

Target
NORDVPN_TOKEN
ConnectVariable

Country/city/server/group, anything `nordvpn connect` accepts. Empty = recommended server.

Target
NORDVPN_CONNECT
SettingsVariable

; separated, each passed to `nordvpn set`.

Target
NORDVPN_SET
Default
killswitch on; technology nordlynx
Value
killswitch on; technology nordlynx
AllowlistVariable

; 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
MeshnetVariable

on to enable Meshnet.

Target
NORDVPN_MESHNET
StatePathrw

Login and Meshnet identity across restarts.

Target
/var/lib/nordvpn
Default
/mnt/user/appdata/nordvpn
Value
/mnt/user/appdata/nordvpn