Deploy F# full-stack applications with Tailscale sidecar for private network access without public ports or authentication. Use when deploying to production, setting up Docker compose with Tailscale,...
Traditional deployments expose services publicly, then add authentication. Tailscale inverts this: your application is private by default, accessible only from your tailnet. No public ports, no attack surface, no authentication to build.
Before deploying, ask:
Core Principles:
No Public Ports: The application binds to internal ports only. Tailscale is the only entry point.
Device-Level Auth: Authentication happens at the Tailscale layer. If you're on the tailnet, you're authorized to access.
Encrypted by Default: All traffic between devices uses WireGuard encryption.
Minimal Configuration: One auth key, one docker-compose file. No nginx, no certbot, no firewall rules.
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Docker Host β
β β
β βββββββββββββββββββ βββββββββββββββββββββββ β
β β Tailscale β β F# Application β β
β β Container βββββΆβ Container β β
β β β β β β
β β TS_HOSTNAME: β β Port: 5000 β β
β β my-app β β (internal only) β β
β ββββββββββ¬βββββββββ βββββββββββββββββββββββ β
β β β
βββββββββββββΌββββββββββββββββββββββββββββββββββββββββ
β
βββββββββΌββββββββ
β Tailnet β
β (WireGuard) β
βββββββββ¬ββββββββ
β
βββββββββΌββββββββ
β Your Devices β
β (laptop, β
β phone, etc) β
βββββββββββββββββ
Access: http://my-app or http://my-app:5000
The pattern: F# app runs in one container, Tailscale in another. Tailscale creates a tunnel; you access the app through that tunnel.
version: '3.8'
services:
# F# Application
app:
build: .
container_name: my-fsharp-app
restart: unless-stopped
environment:
- ASPNETCORE_ENVIRONMENT=Production
- ASPNETCORE_URLS=http://+:5000
- DATA_DIR=/app/data
volumes:
- ./data:/app/data
networks:
- app-network
depends_on:
- tailscale
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5000/api/health"]
interval: 30s
timeout: 3s
retries: 3
start_period: 10s
# Tailscale Sidecar
tailscale:
image: tailscale/tailscale:latest
container_name: my-fsharp-app-tailscale
hostname: my-fsharp-app
restart: unless-stopped
environment:
- TS_AUTHKEY=${TS_AUTHKEY}
- TS_STATE_DIR=/var/lib/tailscale
- TS_HOSTNAME=my-fsharp-app
- TS_ACCEPT_DNS=true
- TS_USERSPACE=false
volumes:
- tailscale-data:/var/lib/tailscale
- /dev/net/tun:/dev/net/tun
cap_add:
- NET_ADMIN
- SYS_MODULE
networks:
- app-network
healthcheck:
test: ["CMD", "tailscale", "status"]
interval: 30s
timeout: 5s
retries: 3
networks:
app-network:
driver: bridge
volumes:
tailscale-data:
App Container:
ASPNETCORE_URLS=http://+:5000 β Listen on all interfaces, port 5000DATA_DIR β Environment-based data directorydepends_on: tailscale β Start after Tailscale is readyTailscale Container:
TS_AUTHKEY β One-time key to join your tailnetTS_HOSTNAME β The name you'll use to access the app/dev/net/tun β Required for VPN tunnelcap_add β Linux capabilities for networkingtskey-auth-)# .env (in project root)
TS_AUTHKEY=tskey-auth-xxxxxxxxxxxxxx
Important: Add .env to .gitignore
# Build and start
docker compose up -d
# Check status
docker compose ps
docker logs my-fsharp-app-tailscale
docker logs my-fsharp-app
# Verify Tailscale connection
docker exec my-fsharp-app-tailscale tailscale status
From any device on your tailnet:
http://my-fsharp-app
http://my-fsharp-app:5000
Install Tailscale on your devices: https://tailscale.com/download
http://your-server:9000)my-fsharp-appTS_AUTHKEY = tskey-auth-xxxxx# Check logs
docker logs my-fsharp-app-tailscale
# Common issues:
# "no tun device" β Ensure /dev/net/tun is mounted
# "auth key invalid" β Generate new key
# "permission denied" β Check NET_ADMIN capability
Fix: No TUN device
# On host, enable TUN support
sudo modprobe tun
# Check app is running
docker logs my-fsharp-app
# Check health
docker exec my-fsharp-app curl -f http://localhost:5000/api/health
# Check network connectivity
docker exec my-fsharp-app-tailscale ping my-fsharp-app
# Key expired? Generate new one at:
# https://login.tailscale.com/admin/settings/keys
# Update .env and restart:
docker compose down
docker compose up -d
tailscale:
environment:
- TS_HOSTNAME=todo-app # Access via http://todo-app
Deploy multiple apps, each with unique hostname:
project-a/
docker-compose.yml # TS_HOSTNAME=project-a
project-b/
docker-compose.yml # TS_HOSTNAME=project-b
Access:
http://project-ahttp://project-bFor HTTPS without certificates:
tailscale:
environment:
- TS_SERVE_CONFIG=/config/serve.json
volumes:
- ./serve.json:/config/serve.json
{
"TCP": {
"443": {
"HTTPS": true
}
},
"Web": {
"my-fsharp-app.your-tailnet.ts.net:443": {
"Handlers": {
"/": {
"Proxy": "http://app:5000"
}
}
}
}
}
β Exposing Public Ports
# BAD: Defeats Tailscale's purpose
ports:
- "5000:5000"
Why bad: Makes app publicly accessible, bypasses Tailscale. Better: Remove port mappings. Access only via Tailscale.
β Committing Auth Keys
# BAD: Secret in repo
environment:
- TS_AUTHKEY=tskey-auth-xxxxx
Why bad: Auth keys in version control.
Better: Use .env file or secrets management.
β Skipping Health Checks
# BAD: No health checks
services:
app:
build: .
# No healthcheck
Why bad: Can't detect failures. Better: Add health checks for monitoring.
β Missing Volume Mounts for Data
# BAD: Data lost on container restart
services:
app:
build: .
# No volumes for data
Why bad: SQLite data disappears.
Better: Mount ./data:/app/data.
Development: Skip Tailscale, use docker compose up with port mapping.
Production single-user: Single Tailscale auth key, ephemeral device.
Team deployment: Reusable key, Tailscale ACLs for access control.
Multi-app server: Separate compose files, unique hostnames.
Match the deployment complexity to your needs.
Before marking deployment complete:
tailscale status)Tailscale removes an entire category of work: no public IPs, no DNS configuration, no TLS certificates, no authentication system. Your app is simply... private. Accessible to you and your devices, invisible to everyone else.
The goal: Deploy once, access from anywhere on your tailnet, worry about nothing else.
/docs/07-BUILD-DEPLOY.md - Docker build guide/docs/08-TAILSCALE-INTEGRATION.md - Detailed Tailscale setup