Tailscale Sidecar Run Deep Dive

-Ben

From my other post , we have a tailscale container running alongside another container. It’s not a long file, but there is a lot of configuration going on. In this post, I’m going to step through the run script line by line.

Full File

First, the full file for reference

#!/usr/bin/env bash

# load our environment variables
source .env

# run our sidecar container
podman run --detach \
  --hostname "$HOSTNAME" \
  --name "$SIDECAR_NAME"\
  --cap-add net_admin \
  --device /dev/net/tun:/dev/net/tun \
  --volume freshrss_tailscale_state:/var/lib/tailscale \
  --env-file .ts.env \
  ghcr.io/tailscale/tailscale:latest

# run our FreshRSS service
podman run --detach \
  --restart unless-stopped \
  --log-opt max-size=10m \
  --network container:"$SIDECAR_NAME" \
  -e TZ=America/New_York \
  -e 'CRON_MIN=1,31' \
  -v freshrss_data:/var/www/FreshRSS/data \
  -v freshrss_extensions:/var/www/FreshRSS/extensions \
  --name "$CONTAINER_NAME" \
  docker.io/freshrss/freshrss

Line by Line

Bash setup

The first few lines are just bash setup.

#!/usr/bin/env bash
run this script through the bash interpreter. After making the script executable1, this tells our computer how to run it.
source .env
We set up a .env file with bash variables2 inside. The source command loads those variables into our current process. We could put any bash in there, but by convention we’re only setting variables.

Tailscale Sidecar Container

podman run --detach \
  --hostname "$HOSTNAME" \
  --name "$SIDECAR_NAME"\
  --cap-add net_admin \
  --device /dev/net/tun:/dev/net/tun \
  --volume freshrss_tailscale_state:/var/lib/tailscale \
  --env-file .ts.env \
  ghcr.io/tailscale/tailscale:latest
podman run --detach
tell our container engine, podman, to run a container detached so we aren’t connected to it’s stdout
--hostname "$HOSTNAME"
set the hostname for the container. Tailscale uses this as the machine name. We’re using the HOSTNAME variable we set in our .env, which makes it easy to share between scripts
--name "$SIDECAR_NAME"
give the container a name. We reference this in our service container, so we’ve made it a variable to ensure it has the same value both places.
--cap-add net_admin
add the net_admin capability to the linux container. This gives the container some priviledges. Tailscale needs it to do some of its networking.
--device /dev/net/tun:/dev/net/tun
give Tailscale access to the tun device so it can network effectively.
--volume freshrss_tailscale_state:/var/lib/tailscale
save the Tailscale state into a volume named “freshrss_tailscale_state”. /var/lib/tailscale is where the information needed for Tailscale to remember this device, so we can teardown and recreate this container at will. This state directory is set with TS_STATE_DIR, which we set in the .ts.env and is loaded by the next line.
--env-file .ts.env
this is the file the holds the environment variables this Tailscale container needs to authenticate and store state in the correct place. env-file is a nice alternative to setting envvars individually, like we do with FreshRSS. It can keep secrets, like our oauth token, out of git (don’t commit your .ts.env file!) and collects all the envvars we need into one place. In fact, we could do this same thing with FreshRSS if we wanted or our configuration got more complex!
ghcr.io/tailscale/tailscale:latest
tells podman which container image to run, “ghcr.io/tailscale/tailscale,” and which version, the “latest” tag. We rely on the command from the Dockerfile this container image was built with to automatically start Tailscale, so we don’t need to specify anything else! ghcr.io is GitHub’s container registry, so we know that’s where tailscale hosts it!

FreshRSS Container

FreshRSS is our main service in the example, but it could be any container you want to run on your tailnet.

# run our FreshRSS service
podman run --detach \
  --restart unless-stopped \
  --log-opt max-size=10m \
  --network container:"$SIDECAR_NAME" \
  -e TZ=America/New_York \
  -e 'CRON_MIN=1,31' \
  -v freshrss_data:/var/www/FreshRSS/data \
  -v freshrss_extensions:/var/www/FreshRSS/extensions \
  --name "$CONTAINER_NAME" \
  docker.io/freshrss/freshrss
podman run --detach
run this container in detached mode too
--restart unless-stopped
set the restart policy for the container. unless-stopped will always try to restart the container unless we explicitly run stop to stop the container. Not really necessary here, since the tailscale sidecar won’t restart this way. But we could have that too if we wanted.
--log-opt max-size=10m
freshrss suggests this setting. It sets the max size of the log file. More info
--network continaer:"SIDECAR_NAME"
this line does the tailscale magic. It makes the tailscale conatiner (with name $SIDECAR_NAME) the networking stack for this container, so our tailscale sidecar can connect to tailscale and talk to the service within this container. We’ve parameterized the name of the tailscale sidecar container so we know we’re using the right one!
-e TZ=America/New_York
sets an environment variable inside the container. In this case, FreshRSS uses the TZ variable to set timezone information, so I’ve set it to the US East Coast.
-e 'CRON_MIN=1,31
another environment variable. This one FreshRSS uses to set when to run a cronjob and update feeds. It’s wrapped in single quotes so we can be sure our shell sends those exact characters through, instead of expanding it somehow.
-v freshrss_data:/var/www/FreshRSS/data
mount a volume in the container. A volume is a place we can store things to persist even when the container has been destroyed. In this case, we’re binding to the directory FreshRSS uses to store its data.
-v freshrss_extensions:/var/www/FreshRSS/extensions
mount another volume into the container. This is the directory FreshRSS uses to save extensions if we load any.
--name "$CONTAINER_NAME"
set the name of the container to the value we stored in $CONTAINER_NAME in our .env file. This makes it much easier to find this container when we run podman ps
docker.io/freshrss/freshrss
the name of the container to run. Note the docker.io on the front here; docker sets this by default if you don’t use it. Other container engines require the full uri (but have settings where you can set defaults).

Wrap Up

That’s the entire file, line by line! Of course you can read all about all these options and more in the docs but hopefully this was a nice little tutorial of all of them in plain English for you. Container engines have a lot of options and the best way to learn is by practice.

Like a pod of seals, float on!

  1. chmod +x run 

  2. key=”value” pairs, where the quotes help in case there’s space!