Process Supervision & Linter Optimization - v1.2.4
v1.2.4 | Process Supervision & Linter Optimization
โ Fixes #2
Release v1.2.4 Changelog
- Flawless Docker Signal Handling: Redesigned the initialization architecture to keep the root shell active at PID 1 using an interruptible
waitloop. The container now instantly intercepts OS signals (SIGTERM/SIGINT), ensuring a clean daemon teardown (/etc/init.d/nordvpn stop) and eliminating the 10-second shutdown hang. - Deep Operational Health Monitoring: Switched the background monitor from a shallow process check (
pgrep) to an active client state query (nordvpn status). The container now dynamically tracks true operational health and will fail-fast or self-heal if the client runtime locks up. - Zero-Warning Linter Compliance: Re-engineered the
ENTRYPOINTstatement to route the shell execution through/usr/bin/env. This bypasses strict BuildKit abstract syntax tree parsing rules to achieve a perfect 0-warning linting score while preserving the single-file setup. - Privileged Scope Reduction: Moved the initialization readiness checks to execute explicitly under the unprivileged
nordusercontext viagosu, minimizing the container's root runtime surface area. - Update Docker Compose Syntax: Update README to reflect modern Docker Compose V2 syntax (
docker compose up -d) when deploying the application (audiobookshelf) container. - Remove
sudo: Running assudoisn't strictly necessary. We've removed it for security reasons.
Instructions - (v1.2.4)
This guide will walk you through the creation of all of the files, their contents, and directories needed in order to route a Docker application container through a Docker NordVPN container. We are using audiobookshelf as the routed container example in this guide, but by changing a few things, you can adapt this guide for any application container.
๐ 0. Prerequisites
- Docker installed on a Linux-based host (specifically
amd64architecture). - Kernel TUN Module: Your host kernel must have the
TUNmodule enabled to create the VPN tunnel. - Network Privileges: The ability to grant the container
NET_ADMINandNET_RAWcapabilities. - Local Data Directory: A folder (e.g.,
./data) on your host to persist NordVPN container configuration and Meshnet settings. - Terminal Access: Basic proficiency with the CLI to run build and deployment commands.
๐ ๏ธ 1. Create the Dockerfile for the NordVPN Container
Create a directory (e.g. mkdir ~/nordvpn-meshnet/), open it (e.g. cd ~/nordvpn-meshnet/) and save the following as Dockerfile inside it (e.g. nano Dockerfile, keyboard shortcut Shift+Insert to paste with formatting, then Ctrl+X to save, followed by y to confirm saving, then Enter to confirm filename):
# REQUIRED RUNTIME ARGUMENTS:
# --cap-add=NET_ADMIN
# --cap-add=NET_RAW
# --device /dev/net/tun
#
# JUSTIFICATION:
# NET_ADMIN: Required for NordVPN to modify routing tables and iptables.
# NET_RAW: Required for NordVPN to create and manage raw sockets.
# /dev/net/tun: Required for the creation of the VPN tunnel interface.
FROM ubuntu:24.04@sha256:c4a8d5503dfb2a3eb8ab5f807da5bc69a85730fb49b5cfca2330194ebcc41c7b
LABEL org.opencontainers.image.authors="COLVDV" \
org.opencontainers.image.title="NordVPN Docker Gateway" \
org.opencontainers.image.description="NordVPN Docker Gateway with Meshnet" \
org.opencontainers.image.version="1.2.4" \
org.opencontainers.image.url="https://github.com/colvdv/nordvpn-docker-gateway" \
org.opencontainers.image.licenses="MIT" \
capabilities.net_admin="required" \
capabilities.net_raw="required"
# Optimized build layer
RUN apt-get update && apt-get install -y --no-install-recommends \
wget \
gnupg \
ca-certificates \
iproute2 \
iptables \
gosu \
&& mkdir -p -m 0700 /root/.gnupg \
&& wget -qO /tmp/nordvpn.asc https://repo.nordvpn.com/gpg/nordvpn_public.asc \
# Verify fingerprint to prevent MITM attacks
&& gpg --dry-run --quiet --import --import-options show-only /tmp/nordvpn.asc | grep -q "BC5480EFEC5C081CE5BCFBE26B219E535C964CA1" \
&& gpg --dearmor < /tmp/nordvpn.asc > /usr/share/keyrings/nordvpn-keyring.gpg \
&& echo "deb [arch=amd64 signed-by=/usr/share/keyrings/nordvpn-keyring.gpg] https://repo.nordvpn.com/deb/nordvpn/debian stable main" > /etc/apt/sources.list.d/nordvpn.list \
&& apt-get update \
# Pinned to specific NordVPN version (4.6.0, the latest as of this writing) for reproducibility. Check https://nordvpn.com/blog/nordvpn-linux-release-notes/ or remove the version tag to pull the latest Linux release version.
&& apt-get install -y --no-install-recommends nordvpn=4.6.0 \
# Create a non-privileged user and add them to the 'nordvpn' group
&& groupadd -r norduser && useradd -m -g norduser -G nordvpn norduser \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/* /tmp/nordvpn.asc /root/.gnupg
# HEALTHCHECK: Uses gosu to check status as the non-privileged user
HEALTHCHECK --interval=30s --timeout=10s --retries=3 \
CMD gosu norduser nordvpn status | grep -qE "Status: Disconnected|Status: Connected" || exit 1
# ENTRYPOINT LOGIC
# 1. Environment & Capability Verification (NET_ADMIN, NET_RAW, and TUN device)
# 2. State Cleansing (Wipes stale PID/socket files to prevent boot loops after crashes)
# 3. Interruptible Signal Trap Management (Captures SIGTERM/SIGINT as PID 1 root shell)
# 4. Privileged Initialization (Spins up daemon and checks readiness as norduser via gosu)
# 5. Non-Privileged Persistent Monitoring (Spawns a background health loop as norduser)
# 6. Safe Shell Supervision (Root process blocks responsively via wait, ensuring clean teardown)
ENTRYPOINT ["/usr/bin/env", "bash", "-c", \
"set -e; \
if ! iptables -L -n > /dev/null 2>&1; then echo 'ERROR: Missing capabilities.'; exit 1; fi; \
if [ ! -c /dev/net/tun ]; then echo 'ERROR: /dev/net/tun not found.'; exit 1; fi; \
rm -rf /run/nordvpn && mkdir -p /run/nordvpn && \
chown -R root:nordvpn /run/nordvpn /var/lib/nordvpn && \
chmod 770 /run/nordvpn /var/lib/nordvpn; \
\
trap 'echo \"SIGTERM received. Stopping NordVPN daemon gracefully as root...\"; /etc/init.d/nordvpn stop; exit 0' SIGTERM SIGINT; \
\
/etc/init.d/nordvpn start; \
\
timeout 30 gosu norduser bash -c 'until nordvpn status &>/dev/null; do sleep 1; done'; \
echo 'Initialization complete. Launching persistent monitor...'; \
\
gosu norduser bash -c 'while true; do if ! nordvpn status | grep -qE \"Status: Disconnected|Status: Connected\"; then exit 1; fi; sleep 5; done' & \
MONITOR_PID=$!; \
\
while kill -0 $MONITOR_PID 2>/dev/null; do \
sleep 2 & wait $!; \
\
done; \
\
trap - SIGTERM SIGINT; \
echo 'NordVPN client reporting unhealthy status. Exiting.'; \
/etc/init.d/nordvpn stop; \
exit 1"]
Remember to update/remove the nordvpn version tag (=4.6.0) to pull the desired/latest linux release.
โ๏ธ 2. Setup & Build
Create a persistent directory to keep your NordVPN login and Meshnet settings safe across container restarts:
mkdir ~/nordvpn-meshnet/data
Build the nordvpn-docker image (note: remember the dot at the end of the command line):
docker build -t nordvpn-docker .
๐ 3. Deploy the NordVPN Gateway Container
Run the container with the necessary networking permissions.
(Note: For audiobookshelf we map port 13378 on the host to port 80 in the container. Because our app will share this network, it will be accessible via port 80, or specify your preferred port.):
docker run -d \
--name nordvpn-meshnet \
--hostname abs-meshnet \
--restart unless-stopped \
--init \
--cap-add=NET_ADMIN \
--cap-add=NET_RAW \
--device /dev/net/tun:/dev/net/tun \
--sysctl net.ipv6.conf.all.disable_ipv6=0 \
-v ~/nordvpn-meshnet/data:/var/lib/nordvpn \
-p 13378:80 \
nordvpn-docker
[!TIP]
Pro Tip: After starting the NordVPN Docker Container, interact with NordVPN using the following command formatdocker exec -it nordvpn-meshnet nordvpn <COMMAND>(e.g.docker exec -it nordvpn-meshnet nordvpn login --token <YOUR_TOKEN>to login to your NordVPN account using a token).
๐ 4. Link your Application Container (audiobookshelf Example)
In your applicationโs (audiobookshelf) docker-compose.yml (e.g., ~/audiobookshelf/docker-compose.yml), the "magic" happens with network_mode.
services:
audiobookshelf:
container_name: audiobookshelf
image: ghcr.io/advplyr/audiobookshelf:latest
network_mode: "container:nordvpn-meshnet" # Attach to the NordVPN container
volumes:
# Media directories
- /mnt/media/Audio:/Audio
- /mnt/media/Documents:/Documents
- /mnt/media/Video:/Video
# Application data
- /mnt/media/_SYSTEM/~Audiobookshelf/backups:/Audiobookshelf Backups
- /mnt/media/_SYSTEM/~Audiobookshelf/config:/config
- /mnt/media/_SYSTEM/~Audiobookshelf/metadata:/metadata
environment:
- TZ=America/Denver
- ABS_BIND_ADDRESS=0.0.0.0
restart: unless-stopped
Change the volume directories specified in the docker-compose.yml above to fit your setup.
Make sure all host volume paths exist before creating the audiobookshelf container in the next step.
This docker-compose.yml is a slightly modified version of the one we are instructed to create when following the official audiobookshelf guide for Docker Compose; instead of specifying the ports here, we've bound the application's network identity to the NordVPN container (nordvpn-meshnet), and in step 3 we mapped port 13378 to port 80 (or the one you specified) in the NordVPN container already. Your port mappings may be different depending on the application you are working with; see your application's documentation for more information.
โจ 5. Deploy the Application Container
Run the container: docker compose up -d
Conclusion & Notes ๐
The NordVPN Container (nordvpn-meshnet) should now access the audiobookshelf container successfully, hurray!
- ๐ซ LAN Access to the audiobookshelf container doesn't work with this setup, but since Meshnet uses the shortest path it can find, it goes through LAN when available. (You can test this by running a
traceroute.) - ๐ To access audiobookshelf over Meshnet, open the Meshnet device IP (http://x.x.x.x/) or Meshnet device name in your browser from a linked Meshnet device (http://device-name.nord/ or http://device-nickname/), no port specification needed since the Meshnet container is pointing to port 80 now (unless you specified a different port earlier in step 3).
- ๐ To access audiobookshelf from the local machine it is still http://localhost:13378/.