These are the notes I made while trying to understand how I can serve a Podman container to my Tailnet. This is part of the research I’m doing around migrating from my old home Linux server to a new one.

Update 2026-07-11: Added notes on how to use the TS_SERVE_CONFIG env var to configure tailscale serve.

Tailscale container

Start the Tailscale container:

mkdir "$HOME/ts-test2"
podman run -d --name ts-test2 \
  --cap-add=NET_ADMIN \
  --device=/dev/net/tun \
  -e TS_AUTHKEY=tskey-auth-XXXXX \
  -e TS_HOSTNAME=ts-test2 \
  -e TS_STATE_DIR=/var/lib/tailscale \
  -v $HOME/ts-test2:/var/lib/tailscale \
  docker.io/tailscale/tailscale:latest

See the container logs and the status of tailscale:

podman logs ts-test2
podman exec ts-test2 tailscale status

Nginx container

Start the Nginx container connected to the ts-test2 network:

podman run -d --name ts-test2-nginx \
  --network=container:ts-test2 \
  docker.io/nginx:latest

Watch the container logs:

podman logs -f ts-test2-nginx

In another terminal on the tailnet, load the Nginx landing page:

curl http://ts-test2/

If you aren’t using Tailscale Magic DNS, you can use the tailnet IP:

curl http://$(tailscale ip --4 ts-test2)

Serve Nginx (via https) over Tailscale

Manually run tailscale serve in the tailscale container:

podman exec ts-test2 tailscale serve --bg 80

Connecting to https://ts-test2.{tailname}.ts.net/ causes the TLS certificate to get issued. That’s been taking about 12s. If testing with curl, bump the timeout since the default is 10s. After the issuance you’ll see the Nginx landing page.

curl -v --connect-timeout 30 https://ts-test2.{tailname}.ts.net/

Replace {tailname} with the name of your tailnet.

Since the tailscale serve config is stored in the tailscale state file (tailscaled.state), it will be restored after reboots. However, this is not the way. We should be using a TS_SERVE_CONFIG file.

Tailscale serve configuration

We can find out what a serve configuration file should look like by running:

podman exec ts-test2 tailscale serve status -json

The output is like:

{
  "TCP": {
    "443": {
      "HTTPS": true
    }
  },
  "Web": {
    "ts-test2.tailnet-name.ts.net:443": {
      "Handlers": {
        "/": {
          "Proxy": "http://127.0.0.1:80"
        }
      }
    }
  }
}

Create a JSON file with this output (e.g, ts-test-serve.json) and replace “ts-test2.tailnet-name.ts.net” with ${TS_CERT_DOMAIN}:

{
  "TCP": {
    "443": {
      "HTTPS": true
    }
  },
  "Web": {
    "${TS_CERT_DOMAIN}:443": {
      "Handlers": {
        "/": {
          "Proxy": "http://127.0.0.1:80"
        }
      }
    }
  }
}

Pass the environment variable TS_SERVE_CONFIG to the container where the value is the path to that file.

Running the container would look like:

mkdir -p "$HOME/ts-test3/state"
mkdir -p "$HOME/ts-test3/config"
cp ts-test-serve.json $HOME/ts-test3/config/ts-serve.json
podman run -d --name ts-test3 \
  --cap-add=NET_ADMIN \
  --device=/dev/net/tun \
  -e TS_AUTHKEY=tskey-auth-XXXXX \
  -e TS_HOSTNAME=ts-test3 \
  -e TS_STATE_DIR=/var/lib/tailscale \
  -e TS_SERVE_CONFIG=/config/ts-serve.json \
  -v $HOME/ts-test3/state:/var/lib/tailscale \
  -v $HOME/ts-test3/config:/config \
  docker.io/tailscale/tailscale:latest

Helpful commands

To watch container logs:

podman logs -f ts-test2
podman logs -f ts-test2-nginx

To see tailscale status:

podman exec ts-test2 tailscale status
podman exec ts-test2 tailscale serve status