Tailscale and Docker networking
After a well deserved rest for the past few weeks I'm back to my normal routine. One of the ideas I've been toying around with (more on that at some point in the future) benefits from remotely accessing resources running on your local network. If you grew up in the 90s or early 00s you probably remember setting up a NAT in your router to forward certain ports so you could play your favorite game with your friends. Tailscale1 has been on my radar for a while so I decided to take it for a spin.
So what is Tailscale?
It is a mesh VPN service that allows you to easily connect to your devices and services. In practical terms this allows you to create a software defined network for securely accessing servers, files or other services in your own devices without having to mess around with the typical network building blocks like firewalls and NAT. These software defined networks are called tailnets.
There is a lot more detail about how Tailscale works behind the scenes 2, and the architecture of the service is quite interesting with the control plane operating as your typical centralized cloud service, but then the data plane is actually a mesh network where each node in your tailnet has a VPN tunnel to each other node which is a super cool approach. In a nutshell nodes in a tailnet register with the centralized service (registering their public keys), allowing them to discover and be discovered by other nodes in the same network. Once this is done, each node establishes a Wireguard3 connection to all the other nodes in the tailnet Beyond allowing you to connect to services in a tailnet, you can also use Tailscale to expose a local service to the Internet, using Funnel4.
Exposing containerized workloads to the Internet
With a baseline understanding of what Tailscale can do, the next question is how can we expose a dockerized workload to the Internet?
After setting up Tailscale (the official guide5 is great BTW), the key thing to keep in mind is that a Tailscale container will be running as a sidecar of your service container, and leverages the network_mode
attribute. Let's look at a basic example based on docker compose:
version: "3.7"
services:
tailscale-nginx:
image: tailscale/tailscale:latest
hostname: tailscale-nginx
environment:
- TS_AUTHKEY=<YOUR-TAILSCALE-AUTH-KEY>
- TS_EXTRA_ARGS=--advertise-tags=tag:container
- TS_STATE_DIR=/var/lib/tailscale
- TS_USERSPACE=false
volumes:
- ${PWD}/tailscale-nginx/state:/var/lib/tailscale
- /dev/net/tun:/dev/net/tun
cap_add:
- net_admin
- sys_module
restart: unless-stopped
nginx:
image: nginx
depends_on:
- tailscale-nginx
network_mode: service:tailscale-nginx
By setting the network_mode
6 attribute in the nginx
service to service:tailscale-nginx
, that service will use the network settings of the tailscale-nginx
service (by sharing the same Linux network namespace7) - hence why in the official tutorial it is crucial to ensure that the network mode matches the service name.
In order to expose a service in a tailnet to the Internet we need to setup Funnel. Again the official instructions8 are very good, the only caveat to keep in mind is that you will be sharing a resource that has the tag:container
set (note the TS_EXTRA_ARGS=--advertise-tags=tag:container
in the docker compose file example above), and so the access control policy in the Tailscale admin console needs to be slightly tweaked compared to the official instructions.
In the original instructions the user would add the following snippet to the policy file:
"nodeAttrs": [
{
"target": ["autogroup:member"],
"attr": ["funnel"],
},
],
In order to work with this Docker example, we need to include the tag:container
in the the nodeAttrs
:
"nodeAttrs": [
{
"target": ["autogroup:member", "tag:container"],
"attr": ["funnel"],
},
],
With this out of the way, we are able to share our containerized services to the Internet, which is pretty neat.
Cool, so what?
A practical application is this is if for you want to share a bunch of photos/videos with friends and family but it's not really convenient for you to use services like Google Photos (maybe you have a ton of photos and videos and it wouldn't make sense to upload everything, or you don't want companies training their generative AI models on your content). In this case an intersting option could be to host your pictures locally and securely share them using Tailscale. This is exactly what I did using Immich9, which already comes with docker compose file that sets everything up for you.
With this in mind the only thing I had to do was to add the Tailscale sidecar to that docker compose file and ensure that the Immich server was using the same network as the sidecar (by setting the network_mode
attribute). This Github repo has the code for reference10.
Footnotes
-
This reads almost like a manifesto and if like me, you experienced the early Web and played around connecting to your friends PCs to play games or share files I think this resonates quite a bit. ↩
-
There are several good resources: Blog post on running Tailscale with Docker, Tailscale with docker docs ↩