Skip to content
Distr
Book DemoStart free trialLogin

Docker Compose with Tailscale

With this setup, a Tailscale container on the customer’s Docker deployment target joins your tailnet, and your support engineers reach the host’s Docker engine through it. Read the Remote Access Overview first. It explains how the remote access application runs next to your application and what access to the Docker socket means.

There are two ways to reach the Docker engine:

The customer’s container joins your tailnet, not the customer’s. Each customer gets a tag, which keeps their container apart from other customers and from the rest of your tailnet. Only your support team may connect to it.

Your support engineers need Tailscale installed on their own machines and signed in to your tailnet. Keep MagicDNS enabled, which it is by default, so that the device names used below resolve.

Add a group for your support team, a tag for the customer, a grant and an SSH rule to the tailnet policy file:

policy.hujson
{
"groups": {
"group:support": ["alice@example.com", "bob@example.com"]
},
"tagOwners": {
"tag:customer-acme": ["autogroup:admin"]
},
"grants": [
{
"src": ["group:support"],
"dst": ["tag:customer-acme"],
"ip": ["tcp:22", "tcp:2375"]
}
],
"ssh": [
{
"action": "check",
"src": ["group:support"],
"dst": ["tag:customer-acme"],
"users": ["root"],
"checkPeriod": "12h"
}
]
}

The grant opens port 22 of the customer’s container for Tailscale SSH and port 2375 for the forwarded socket, and only to your support team. The SSH rule logs them in as root in that container, and check makes them authenticate again every 12 hours.

For the next customer, add another tag such as tag:customer-globex with its own grant and SSH rule.

In the Tailscale admin console, generate an auth key under Settings > Keys with these settings:

  • Reusable, so the container can register again after a restart.
  • Ephemeral, so the device disappears from your tailnet shortly after the container stops.
  • Pre-approved, if your tailnet requires device approval.
  • Tags: tag:customer-acme.

Open Customers, click Manage secrets next to the customer and create the secret TAILSCALE_AUTHKEY with the auth key as its value. The Compose files below read it through the .env template.

Tailscale SSH runs inside the Tailscale container, and that is where your support engineer’s shell opens. The official image has no Docker CLI, so you build a small image that adds it.

Dockerfile
FROM tailscale/tailscale:v1.104.4
RUN apk add --no-cache docker-cli

Build it for the platforms of your customers and push it to the Distr registry:

Terminal window
docker buildx build --platform linux/amd64,linux/arm64 \
--tag registry.distr.sh/your-org/remote-access:1.104.4 --push .

The Distr controller pulls images from the Distr registry with its own credentials. If you use artifact entitlements, grant the customer the image. You can also make it a public artifact, because it contains nothing but Tailscale and the Docker CLI.

Create a Docker application called Remote Access and add a version with this Compose file and this Template for Environment File:

services:
tailscale:
image: registry.distr.sh/your-org/remote-access:1.104.4
environment:
TS_AUTHKEY: ${TS_AUTHKEY:?}
TS_HOSTNAME: ${TS_HOSTNAME:?}
TS_EXTRA_ARGS: --ssh
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped

TS_EXTRA_ARGS: --ssh turns on the Tailscale SSH server. The mounted socket connects the Docker CLI in the container to the host’s Docker engine. TS_HOSTNAME is the name of the device in your tailnet; set a different one for each customer when you deploy.

Deploy the application to the customer’s deployment target with + Deploy App. Once the deployment is running, the device acme-debug appears in your tailnet, and your support engineer connects to it:

Terminal window
tailscale ssh root@acme-debug

In that shell, the Docker CLI talks to the host’s Docker engine:

Terminal window
docker ps
docker logs --tail 100 distr-1a2b3c4d-backend-1
docker exec -it distr-1a2b3c4d-backend-1 sh

The Distr controller runs each deployment as a Compose project named distr-<short-deployment-id>, and the container names start with that prefix.

Here your support engineer works with the Docker CLI on their own machine, where their usual tools and scripts are. You do not need an image of your own for this option.

Create a Docker application called Remote Access and add a version with this Compose file and this Template for Environment File:

services:
tailscale:
image: tailscale/tailscale:v1.104.4
environment:
TS_AUTHKEY: ${TS_AUTHKEY:?}
TS_HOSTNAME: ${TS_HOSTNAME:?}
TS_SERVE_CONFIG: /config/serve.json
configs:
- source: tailscale-serve
target: /config/serve.json
restart: unless-stopped
docker-socket:
image: alpine/socat:1.8.1.3
command: tcp-listen:2375,bind=127.0.0.1,fork,reuseaddr unix-connect:/var/run/docker.sock
network_mode: service:tailscale
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: unless-stopped
configs:
tailscale-serve:
content: |
{"TCP": {"2375": {"TCPForward": "127.0.0.1:2375"}}}

The docker-socket container shares the network of the tailscale container and serves the mounted socket on 127.0.0.1:2375. Neither the host nor any other container can reach that address. Tailscale Serve forwards port 2375 of the device in your tailnet to it, as configured in the TS_SERVE_CONFIG file. Deploy this version in regular Compose mode, because Docker Swarm does not support network_mode: service:.

Deploy the application to the customer’s deployment target with + Deploy App. Your support engineer then creates a Docker context for the customer on their own machine and uses it like a local Docker engine:

Terminal window
docker context create acme --docker host=tcp://acme-debug:2375
docker --context acme ps
docker --context acme logs --tail 100 distr-1a2b3c4d-backend-1
docker --context acme exec -it distr-1a2b3c4d-backend-1 sh

The Docker API on port 2375 is unencrypted. The connection still runs inside Tailscale’s WireGuard tunnel, and only devices the grant admits can open it.

Undeploy the remote access application when the support case is closed. The container stops, and Tailscale removes the ephemeral device from your tailnet shortly after. To lock out a customer’s container right away, remove the device in the Tailscale admin console and revoke the auth key. Create a new key and update the secret before the next support case. Your support engineer removes the Docker context with docker context rm acme.