In this article I describe the steps I took for running a dockerised version of the zerotier-one client on my Synology NAS DSM 7.0, using docker.

I use ZeroTier One for joining together some/most of my devices, be them a local laptop, my mobile phone, or a remote VPS somewhere.

It’s a useful technology that provides a safe and secure link between those devices, so I can access services hosted on them without exposing those same services to the outside world.

I use it to run a personal wiki, as well as a few other web applications that are for my own usage only.

I wanted my NAS to also be available inside that network, so I could share its docker-based private registry across my fleet of systems, as well as being able to back all those systems to the nas via a simple tar and rsync.

My Synology NAS runs the DSM 7.0 beta, and the ZeroTier One package, built for version 6, no longer works in it. Fortunately, DSM 7.0 has a Docker package which works, and with some sweat and tears it’s possible to make it run properly.

Firstly, we need to build the Docker container for zerotier-one. I dislike using other people’s docker images, so I built my own using this Dockerfile:

FROM debian:buster-slim
LABEL com.darkpan.github-check github.com/zerotier/ZeroTierOne ZT_VERSION
ENV ZT_VERSION 1.6.5
RUN echo "deb http://deb.debian.org/debian buster main non-free contrib" > /etc/apt/sources.list
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
RUN apt-get update && apt-get install -y \
        ca-certificates \
        curl \
        gnupg2 \
    --no-install-recommends \
    && curl -sSL 'https://download.zerotier.com/contact%40zerotier.com.gpg' | apt-key add - \
    && echo "deb http://download.zerotier.com/debian/buster buster main" > /etc/apt/sources.list.d/zerotier-one.list \
    && apt-get update && apt-get install -y \
        zerotier-one \
    --no-install-recommends \
    && apt-get purge --auto-remove -y curl gnupg2 \
    && rm -rf /var/lib/apt/lists/*
ENTRYPOINT ["/bin/bash"]

I created a ~/zt/ directory on my NAS where I put the above Dockerfile.

I then built a local “zerotier-one” image using it, i.e.:

cd ~/zt
sudo docker build -t zerotier-one .

Next up, running it. The service requires a few things: firstly, and most importantly, a TUN/TAP device.

This isn’t available by default during the boot, so I had to ensure the proper kernel module is available and started after every reboot.

I use a single “launch” script to ensure the tun device is created and the container is started properly after a reboot. To use it, put it somewhere on the filesystem, then use the “Task Scheduler” feature to have it run after boot, as the root user.

#!/bin/bash
set -x
HOME=/root
ZT_NAME=zerotier-one
ZT_LOCAL_VOLUME="$HOME/zt/zerotier-one"
ZT_IMAGE=zerotier-one
DOCKER=/usr/local/bin/docker
mkdir -p "$ZT_LOCAL_VOLUME"
# Wait for docker to be started
timeout=300
docker_started=
while [[ -z "$docker_started" ]]; do
  sleep 1
  docker_started=$(ps faxuww | grep '/var/packages/Docker[/]target/usr/bin/dockerd')
  timeout=$((timeout-1))
  if [[ "$timeout" == "0" ]]; then
    break
  fi
done
# Give the daemon time to actually start properly...
sleep 5
$DOCKER stop "$ZT_NAME" >/dev/null 2>&1
$DOCKER rm "$ZT_NAME" >/dev/null 2>&1
# Create the necessary file structure for /dev/net/tun
# Load the tun module if not already loaded
if ( !(lsmod | grep -q "^tun\s") ); then
  insmod /lib/modules/tun.ko >/dev/null 2>&1
fi
mkdir -m 755 /dev/net >/dev/null 2>&1
mknod /dev/net/tun c 10 200 >/dev/null 2>&1
chmod 0666 /dev/net/tun >/dev/null 2>&1
state=$($DOCKER inspect --format "{{.State.Running}}" "$ZT_NAME" 2>/dev/null)
if [[ "$state" == "true" ]]; then
    $DOCKER stop "$ZT_NAME" && $DOCKER rm "$ZT_NAME" && $0 "$@"
    exit $?
fi
if [[ -n "$1" ]]; then
    $DOCKER run -it --rm \
        --device=/dev/net/tun --net host --cap-add=NET_ADMIN --cap-add=SYS_RAWIO \
        --name "$ZT_NAME" \
        --memory='128MB' \
        --entrypoint /bin/bash \
        -v "$ZT_LOCAL_VOLUME:/var/lib/zerotier-one" \
        "$ZT_IMAGE" >/dev/null 2>&1
    exit 0
fi
exec $DOCKER run -d --restart=always \
    --device=/dev/net/tun --net host --cap-add=NET_ADMIN --cap-add=SYS_RAWIO \
    --name "$ZT_NAME" \
    --memory='128MB' \
    --entrypoint zerotier-one \
    -v "$ZT_LOCAL_VOLUME:/var/lib/zerotier-one" \
    "$ZT_IMAGE" >/dev/null 2>&1

Running it (sudo ./launch) just starts the container, but doesn’t yet join you to any network.

To do so, “enter” the container and join the network:

$ sudo docker exec -it zerotier-one /bin/bash
# zerotier-cli join f0f0f0f0f0f0f0

If all goes well, you should be able to see the network as having properly been joined:

# zerotier-cli listnetworks
200 listnetworks <nwid> <name> <mac> <status> <type> <dev> <ZT assigned ips>
200 listnetworks f0f0f0f0f0f0f0 blah f0:f0:f0:f0:f0 OK PRIVATE ztrta2fcse 999.88.77.66/16

And that’s it. Enjoy your NAS being part of your ZeroTier one network.