Docker Compose + Tailscale
Run a private Phase Console instance inside your Tailscale network (tailnet) with Docker Compose.
This guide extends the standard Docker Compose deployment with a Tailscale sidecar container. It gives you:
- Private ingress: the Phase Console joins your tailnet as its own machine (e.g.
phase) and is reachable athttps://phase.<your-tailnet>.ts.netfrom any device on your tailnet — with a valid, automatically provisioned TLS certificate via Tailscale Serve. You don't need Tailscale installed on the Docker host itself. Optionally, you can expose the Console to the public internet through Tailscale Funnel — no reverse proxy, DNS records or certificate management required. - Private egress: the Phase
backendandworkercontainers can reach other machines on your tailnet directly. This lets secret syncs and other integrations talk to private services — a self-hosted GitLab, Kubernetes cluster, an internal LLM gateway, a database — without exposing any of them to the internet.
Prerequisites
- A Tailscale network (tailnet) with:
- MagicDNS enabled (default for new tailnets)
- HTTPS certificates enabled — required by Tailscale Serve. Enable it in the Tailscale admin console under DNS → HTTPS Certificates.
- Docker and Docker Compose installed on the host (see Docker Compose for installation steps).
Generate a Tailscale auth key
The sidecar authenticates to your tailnet with an auth key:
- Open the Keys page in the Tailscale admin console and click Generate auth key.
- Leave Reusable off (the key authenticates a single machine).
- Leave Ephemeral off — the Phase machine must persist across container restarts.
- If your tailnet uses device approval, enable Pre-approved.
- Copy the generated
tskey-auth-…key. You'll add it to your.envfile below.
The auth key is only used on first boot. The sidecar persists its identity in
a Docker volume (phase-tailscale-state), so restarts and re-deployments do
not consume the key again — even after it expires.
1. Download the configuration files
Create a working directory:
mkdir -p phase-tailscale/nginx phase-tailscale/tailscale && cd phase-tailscale
.env template:
wget -O .env https://raw.githubusercontent.com/phasehq/console/main/.env.example
Docker Compose template:
You can review the Tailscale docker compose configuration 👉 here.
wget -O docker-compose.yml https://raw.githubusercontent.com/phasehq/console/main/tailscale-docker-compose.yml
Nginx Dockerfile & config:
wget -O ./nginx/Dockerfile https://raw.githubusercontent.com/phasehq/console/main/nginx/Dockerfile && \
wget -O ./tailscale/nginx.conf https://raw.githubusercontent.com/phasehq/console/main/tailscale/nginx.conf
Tailscale Serve config — terminates TLS for your tailnet with a valid certificate and proxies to nginx:
wget -O ./tailscale/serve.json https://raw.githubusercontent.com/phasehq/console/main/tailscale/serve.json
2. Update your configuration
Set the host and Tailscale-specific values in your .env, replacing <your-tailnet> with your tailnet name (e.g. tail1a2b3c.ts.net):
.env
HOST=phase.<your-tailnet>.ts.net
HTTP_PROTOCOL=https://
# Hostnames the backend will accept. 'backend' is required for
# internal service-to-service calls. Add your host's LAN name if needed.
ALLOWED_HOSTS=phase.<your-tailnet>.ts.net,localhost,backend
ALLOWED_ORIGINS=https://phase.<your-tailnet>.ts.net,https://localhost
# Tailscale auth key (see Prerequisites)
TS_AUTHKEY=tskey-auth-…
Generate secrets
sed -i.bak "s|DATABASE_PASSWORD=.*|DATABASE_PASSWORD=$(openssl rand -hex 32)|g" .env && \
sed -i.bak "s|SECRET_KEY=.*|SECRET_KEY=$(openssl rand -hex 32)|g" .env && \
sed -i.bak "s|SERVER_SECRET=.*|SERVER_SECRET=$(openssl rand -hex 32)|g" .env && \
sed -i.bak "s|NEXTAUTH_SECRET=.*|NEXTAUTH_SECRET=$(openssl rand -hex 32)|g" .env && \
rm .env.bak
For a complete list of available options, refer to the environment variables documentation.
3. Start services
docker compose up -d
On first boot, the tailscale service joins your tailnet using TS_AUTHKEY. Check that it's connected:
docker compose exec tailscale tailscale status
4. Configure access
This is the default behaviour of the downloaded serve.json — the Console is reachable only from devices on your tailnet (subject to your tailnet ACLs).
Check that Tailscale Serve is configured:
docker compose exec tailscale tailscale serve status
# https://phase.<your-tailnet>.ts.net (tailnet only)
# |-- / proxy http://127.0.0.1:80
From any device on your tailnet, check the health endpoints — no certificate-validation flags needed, since Tailscale Serve provisions a valid certificate (the first request after startup can take a few seconds while the certificate is issued):
curl https://phase.<your-tailnet>.ts.net/service/health/
# {"status": "alive", "version": "x.x.x"}
curl https://phase.<your-tailnet>.ts.net/api/health
# {"status":"alive"}
Open https://phase.<your-tailnet>.ts.net in a browser on any tailnet device to create your account.
The Console also remains reachable directly on the Docker host at https://localhost (self-signed certificate, so your browser will show a warning and curl needs -k). For strict tailnet-only access — no ports exposed on the Docker host at all — remove the ports section from the tailscale service in docker-compose.yml and run docker compose up -d.
Operations
Custom machine name
The tailnet machine name comes from the hostname of the tailscale service. Change hostname: phase to e.g. hostname: secrets to serve the Console at https://secrets.<your-tailnet>.ts.net, and update HOST, ALLOWED_HOSTS and ALLOWED_ORIGINS in your .env accordingly.
Restarting the Tailscale sidecar
The nginx, backend and worker containers live inside the tailscale service's network namespace and must re-attach whenever it changes:
- If the
tailscalecontainer was restarted (e.g. crashed and came back):docker compose restart nginx backend worker - If the
tailscalecontainer was recreated (e.g. after a config change):docker compose up -drecreates the dependent services automatically
Updating
docker compose pull && docker compose up -d
The Tailscale machine identity is kept in the phase-tailscale-state volume, so updates do not require a new auth key.
Uninstall
To remove the deployment including all data:
docker compose down -v
Then remove the phase machine from your tailnet in the Tailscale admin console.
Troubleshooting
/dev/net/tun not available
Kernel-mode networking requires the TUN device on the Docker host. On most Linux hosts it exists by default; if not, load the module with modprobe tun and ensure it loads at boot.
tailscale container restarting in a loop
Without a valid TS_AUTHKEY, the sidecar's login attempt times out and the container exits and restarts repeatedly — and the services sharing its network namespace lose connectivity. Check the logs:
docker compose logs tailscale
If the auth key is invalid or expired, generate a new one, update .env, and run:
docker compose up -d --force-recreate tailscale && docker compose up -d
Certificate errors on the tailnet hostname
Tailscale Serve requires HTTPS Certificates to be enabled for your tailnet (admin console → DNS → HTTPS Certificates). The first HTTPS request after startup can take a few seconds while the certificate is provisioned.
400 Bad Request from the backend
The hostname you're using isn't in ALLOWED_HOSTS. Add it (comma-separated) in .env and run docker compose up -d to apply.
Testing connectivity to services on your tailnet
The backend and worker containers have transparent access to your tailnet, with MagicDNS resolution — this is what lets integrations like secret syncs and dynamic secrets reach private services (a self-hosted GitLab, a Kubernetes cluster, an internal LLM gateway) via their tailnet hostnames, without exposing them to the internet. You can verify connectivity from inside the containers:
docker compose exec backend curl http://<machine-name>:<port>/
docker compose exec worker curl http://<machine-name>:<port>/
where <machine-name> is any machine on your tailnet (check tailscale status for the list).
Tailnet hostnames don't resolve from backend/worker
MagicDNS inside the shared network namespace requires TS_ACCEPT_DNS: "true" on the tailscale service. Verify the resolver configuration:
docker compose exec backend cat /etc/resolv.conf
# nameserver 100.100.100.100
# search <your-tailnet>.ts.net
Health checks
Identical to the standard deployment — see Docker Compose troubleshooting. On the tailnet hostname, omit -k (the certificate is valid); on https://localhost, keep it.