Overview
ScaleTail provides a standardized template for creating custom service configurations with Tailscale sidecar containers. This template ensures consistency, maintainability, and easy migration from existing Docker Compose stacks.
The sidecar pattern allows any containerized service to securely connect to your Tailscale network without modifying the application container itself.
Quick Start
Create a new custom service in three steps:
Copy the template
Update the configuration
Deploy the stack
# 1. Copy template
cp -r templates/service-template services/my-service
cd services/my-service
# 2. Edit compose.yaml and .env
vim compose.yaml
vim .env
# 3. Deploy
docker compose up -d
Service Template Structure
The template provides a complete Docker Compose configuration with Tailscale sidecar:
configs :
ts-serve :
content : |
{"TCP":{"443":{"HTTPS":true}},
"Web":{"${TS_CERT_DOMAIN}:443":
{"Handlers":{/":
{"Proxy":"http://127.0.0.1:80"}}}},
"AllowFunnel":{"${TS_CERT_DOMAIN}:443":false}}
services :
# Tailscale Sidecar Configuration
tailscale :
image : tailscale/tailscale:latest
container_name : tailscale-${SERVICE}
hostname : ${SERVICE}
environment :
- TS_AUTHKEY=${TS_AUTHKEY}
- TS_STATE_DIR=/var/lib/tailscale
- TS_SERVE_CONFIG=/config/serve.json
- TS_USERSPACE=false
- TS_ENABLE_HEALTH_CHECK=true
- TS_LOCAL_ADDR_PORT=127.0.0.1:41234
#- TS_ACCEPT_DNS=true # Uncomment when using MagicDNS
- TS_AUTH_ONCE=true
configs :
- source : ts-serve
target : /config/serve.json
volumes :
- ./config:/config
- ./ts/state:/var/lib/tailscale
devices :
- /dev/net/tun:/dev/net/tun
cap_add :
- net_admin
#ports:
# - 0.0.0.0:${SERVICEPORT}:${SERVICEPORT}
healthcheck :
test : [ "CMD" , "wget" , "--spider" , "-q" , "http://127.0.0.1:41234/healthz" ]
interval : 1m
timeout : 10s
retries : 3
start_period : 10s
restart : always
# Application
application :
image : ${IMAGE_URL}
network_mode : service:tailscale # Sidecar pattern
container_name : app-${SERVICE}
environment :
- PUID=1000
- PGID=1000
- TZ=Europe/Amsterdam
volumes :
- ./${SERVICE}-data/app/config:/config
depends_on :
tailscale :
condition : service_healthy
healthcheck :
test : [ "CMD" , "pgrep" , "-f" , "${SERVICE}" ]
interval : 1m
timeout : 10s
retries : 3
start_period : 30s
restart : always
Step-by-Step Configuration Guide
Step 1: Copy the Template
Copy the service template to a new directory:
cp -r templates/service-template services/ < service-nam e >
cd services/ < service-nam e >
Create a .env file with your service-specific variables:
# Service Configuration
SERVICE = my-service
SERVICEPORT = 8080
IMAGE_URL = linuxserver/my-service:latest
# Tailscale Configuration
TS_AUTHKEY = tskey-auth-xxxxx-xxxxxxxxxxxxxxxx
TS_CERT_DOMAIN = my-service.tail1234.ts.net
# Optional DNS Configuration
DNS_SERVER = 1.1.1.1
Never commit real auth keys or secrets to version control. Set TS_AUTHKEY and other sensitive values in .env and add .env to .gitignore.
Step 3: Update Compose Configuration
Modify compose.yaml based on your service requirements:
Container Names
Keep the standardized naming convention:
tailscale :
container_name : tailscale-${SERVICE}
application :
container_name : app-${SERVICE}
Network Mode (Critical)
Always keep the sidecar pattern:
application :
network_mode : service:tailscale # Routes app through Tailscale
depends_on :
tailscale :
condition : service_healthy
The network_mode: service:tailscale configuration routes all application traffic through the Tailscale container, eliminating the need to modify the application itself.
Tailscale Serve Configuration
Update the proxy target port to match your application’s internal port:
configs :
ts-serve :
content : |
{"TCP":{"443":{"HTTPS":true}},
"Web":{"${TS_CERT_DOMAIN}:443":
{"Handlers":{/":
{"Proxy":"http://127.0.0.1:8080"}}}}, # Change port here
"AllowFunnel":{"${TS_CERT_DOMAIN}:443":false}}
The Tailscale Serve config does not consume .env values automatically. You must manually update the port number in the JSON.
If you don’t need Tailscale Serve/Funnel, remove the configuration:
# Remove the entire configs: section
# Remove from tailscale service:
environment :
# - TS_SERVE_CONFIG=/config/serve.json # Remove this line
configs : # Remove this section
- source : ts-serve
target : /config/serve.json
Volumes
Adjust volume mounts for your service:
application :
volumes :
- ./my-service-data/config:/config
- ./my-service-data/media:/media
- ./my-service-data/downloads:/downloads
Best practice: Pre-create bind-mount paths to avoid Docker creating root-owned folders:
mkdir -p my-service-data/{config,media,downloads}
chown -R $( id -u ) : $( id -g ) my-service-data
Devices and Capabilities
If your service needs special hardware access, add devices explicitly:
application :
devices :
- /dev/dri:/dev/dri # GPU access
- /dev/fuse:/dev/fuse # FUSE filesystem
group_add :
- "video" # Video group for GPU
- "render" # Render group for GPU
Mention these requirements in your README so users know they need specific hardware or group membership.
Port Exposure (Optional)
By default, ports are commented out to keep services Tailscale-only:
#ports:
# - 0.0.0.0:${SERVICEPORT}:${SERVICEPORT}
Uncomment only if LAN exposure is required and explain why in the README:
ports :
- 0.0.0.0:${SERVICEPORT}:${SERVICEPORT} # Required for local device discovery
Step 4: Health Checks
Customize the application health check:
application :
healthcheck :
test : [ "CMD" , "curl" , "-f" , "http://localhost:8080/health" ]
interval : 1m
timeout : 10s
retries : 3
start_period : 30s
Common health check patterns:
# HTTP endpoint
test : [ "CMD" , "curl" , "-f" , "http://localhost:8080/health" ]
# Process check
test : [ "CMD" , "pgrep" , "-f" , "my-service" ]
# TCP port check
test : [ "CMD" , "nc" , "-z" , "localhost" , "8080" ]
# Script check
test : [ "CMD" , "/healthcheck.sh" ]
Step 5: Create Service README
Document your service using the template structure:
# My Service with Tailscale
Brief description of the service and why Tailscale helps.
## Prerequisites
- User must be in the `docker` group
- GPU access requires `video` and `render` groups
- Service requires 2GB free disk space
## Configuration
1. Copy `.env.example` to `.env`
2. Update `TS_AUTHKEY` with your Tailscale auth key
3. Set `SERVICEPORT` to desired port (default: 8080)
## Deployment
```bash
docker compose up -d
Access
Gotchas
Initial admin setup requires accessing web UI
Default credentials: admin/admin (change immediately)
Config directory must be named exactly config
Requires specific group ID 1000 for file permissions
MagicDNS/HTTPS
Uncomment TS_ACCEPT_DNS=true in compose.yaml to use MagicDNS.
Additional Resources
### Step 6: Validate Configuration
Before deploying, validate your configuration:
```bash
# Check for syntax errors and missing variables
docker compose config
# If successful, you'll see the interpolated configuration
# If errors, Docker will report missing variables or syntax issues
Configuration Best Practices
From the ScaleTail contributing guidelines (CONTRIBUTING.md):
Keep the sidecar pattern intact
Always maintain:
network_mode: service:tailscale in application container
depends_on using Tailscale health check
Health checks for both containers
application :
network_mode : service:tailscale # Never remove
depends_on :
tailscale :
condition : service_healthy # Ensures Tailscale is ready
Never commit to version control:
Real auth keys (TS_AUTHKEY)
Credentials
.env files with real values
Use .env.example with placeholder values instead.
Keep ports commented unless required
Document all prerequisites
In your README, list:
Required user groups
Hardware requirements (GPU, devices)
Storage requirements
Default credentials
Initial setup steps
Common gotchas
Follow the naming conventions:
Container: tailscale-${SERVICE} and app-${SERVICE}
Hostname: ${SERVICE}
Volumes: ./${SERVICE}-data/
Common Customization Patterns
Pattern 1: Simple Web Service
Minimal configuration for a basic web app:
services :
tailscale :
# ... standard tailscale config ...
application :
image : nginx:latest
network_mode : service:tailscale
container_name : app-${SERVICE}
volumes :
- ./html:/usr/share/nginx/html:ro
depends_on :
tailscale :
condition : service_healthy
restart : always
Pattern 2: Database Service
Service with persistent data and no HTTP interface:
services :
tailscale :
# ... standard tailscale config ...
# Remove TS_SERVE_CONFIG if no web interface
application :
image : postgres:16
network_mode : service:tailscale
container_name : app-${SERVICE}
environment :
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_USER=${DB_USER}
- POSTGRES_DB=${DB_NAME}
volumes :
- ./postgres-data:/var/lib/postgresql/data
depends_on :
tailscale :
condition : service_healthy
healthcheck :
test : [ "CMD" , "pg_isready" , "-U" , "${DB_USER}" ]
interval : 30s
timeout : 5s
retries : 3
restart : always
Service requiring hardware acceleration:
services :
tailscale :
# ... standard tailscale config ...
application :
image : linuxserver/plex:latest
network_mode : service:tailscale
container_name : app-${SERVICE}
environment :
- PUID=1000
- PGID=1000
- VERSION=docker
volumes :
- ./config:/config
- ./media:/media
- ./transcode:/transcode
devices :
- /dev/dri:/dev/dri # Intel Quick Sync
group_add :
- video
- render
depends_on :
tailscale :
condition : service_healthy
restart : always
Troubleshooting
Service Not Accessible on Tailnet
Check Tailscale container is healthy:
docker ps --filter name=tailscale- ${ SERVICE }
Verify application is using Tailscale network:
docker inspect app- ${ SERVICE } | grep NetworkMode
# Should show: "NetworkMode": "container:tailscale-${SERVICE}"
Check application logs:
docker logs app- ${ SERVICE }
Application Can’t Start
Verify Tailscale is healthy first:
docker logs tailscale- ${ SERVICE }
The application won’t start until Tailscale passes its health check.
Check environment variables:
docker compose config | grep -A 20 application
Permission Denied Errors
Fix volume ownership:
sudo chown -R $( id -u ) : $( id -g ) ./ ${ SERVICE } -data
Check PUID/PGID match your user:
id
# Update compose.yaml with your UID/GID
Health Check Failing
Test health check manually:
docker exec app- ${ SERVICE } curl -f http://localhost:8080/health
Increase start period if service is slow to start:
healthcheck :
start_period : 60s # Give more time for initialization
Migration from Existing Stacks
To migrate an existing Docker Compose service to ScaleTail:
Copy your existing compose.yaml to services/<service-name>/
Add the Tailscale sidecar from the template
Update the application container:
application : # Your existing service
network_mode : service:tailscale # Add this
depends_on : # Add this
tailscale :
condition : service_healthy
Remove port bindings (optional - keep if LAN access needed)
Test the configuration:
docker compose config
docker compose up -d
Exit Nodes Configure Tailscale exit nodes
MagicDNS Use MagicDNS with your services
Security Best Practices Secure your deployment
Quick Start Deploy your first service
Additional Reading