SELF-HOSTING

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 at https://phase.<your-tailnet>.ts.net from 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 backend and worker containers 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.
Loading diagram...

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:

  1. Open the Keys page in the Tailscale admin console and click Generate auth key.
  2. Leave Reusable off (the key authenticates a single machine).
  3. Leave Ephemeral off — the Phase machine must persist across container restarts.
  4. If your tailnet uses device approval, enable Pre-approved.
  5. Copy the generated tskey-auth-… key. You'll add it to your .env file below.

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 tailscale container was restarted (e.g. crashed and came back): docker compose restart nginx backend worker
  • If the tailscale container was recreated (e.g. after a config change): docker compose up -d recreates 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 → DNSHTTPS 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.