Using Tailscale with Docker
Tailscale is a really nice product that combines the modern VPN capabilities of Wireguard with a really slick and well thought out user experience.
I’ve been using it for personal projects for a short while, and it feels like a technology that I’ll be very happy to stick with over the long term.
It’s free for personal use, very easy to use, and I’m confident in its security, so I’d highly recommend it to others.
Background #
One thing I’ve been meaning to try for a while is exposing a docker container over a Tailscale network. It’s easy to just run a container on a server exposed on a particular host port, and then access that via Tailscale.
However, I’d prefer to expose individual Docker containers rather than the entire host’s services. This way, we can access our containers in a ‘host agnostic’ manner: we don’t need to think about where a container is running or which of a host’s limited ports belongs to which container — it’s just there on our private network.
Solution #
The following docker-compose.yml
file demonstrates one approach, essentially setting up a Tailscale sidecar container for our service:
Example docker-compose.yml
file — customisations are an exercise for the reader!
version: "2.4"
services:
tailscale:
hostname: myservice # This will become the tailscale device name
image: richnorth/tailscale:v0.99.1
volumes:
- "./tailscale_var_lib:/var/lib" # State data will be stored in this directory
- "/dev/net/tun:/dev/net/tun" # Required for tailscale to work
cap_add: # Required for tailscale to work
- net_admin
- sys_module
command: tailscaled
myservice-container:
image: nginx # Your image goes here
network_mode: service:tailscale
Notice that we:
- start a Tailscale container, with the requisite configuration to allow Tailscale to work in Docker.
- start our service container (the one we actually want exposed) alongside. We choose for this container to share a network namespace with the tailscale container — i.e. these containers share a common network interface.
After starting the pair of containers, we need to authenticate the Tailscale daemon with our Tailscale account:
$ docker-compose up
...
$ docker-compose exec tailscale tailscale up
To authenticate, visit:
https://login.tailscale.com/a/SOME_HEX_CODE
Tailscale gives us a login URL, which is used to authenticate this new Dockerised Tailscale daemon to your private Tailscale account.
After completing login, Tailscale will store a small amount of state data inside the tailscale_var_lib
volume-mounted directory. This data needs to be retained between restarts of the containers to avoid having to repeat the login process.
We can find the service’s Tailscale IP address via the admin console:
If we were using the nginx
example service given above, we can now simply access http://SERVICE_ADDRESS from any device connected to our Tailscale network. Notice that there’s no explicit need to share the container’s ports, and we can migrate this container + sidecar to another host as long as we move the tailscale_var_lib
directory or perform the Tailscale login step again.
And that’s it! Our service is just another server on our private Tailscale network, ready for use!
Followup thoughts #
- Running a VPN sidecar container for every service process is not the most efficient approach from a memory/CPU perspective. Tailscale does seem to sip resources, but I’d like to see how it behaves under heavy usage.
- Obviously in this setup we can potentially have multiple Tailscale instances on the same host, all of which lean on the same kernel’s Wireguard implementation. I’ve not yet found evidence that this can be problematic — it certainly seems to at least work with 2+ instances. However, I’d be interested to see if there are any limitations which apply when running more containers simultaneously.
- This solution works for individual one-off containers, but for cases where many containers need to be run, it’s more likely to be worth investing time in creating a Docker network that is fully routed through Wireguard. This post suggests such an approach.