Hi there! I'm Shrijith Venkatrama, founder of Hexmos. Right now, I’m building LiveAPI, a first of its kind tool for helping you automatically index API endpoints across all your repositories. LiveAPI helps you discover, understand and use APIs in large tech infrastructures with ease.
If you’re a developer looking to build a secure, scalable, and easy-to-manage dev server, combining Tailscale, Caddy, and Docker is a great way to do it. This stack lets you create a private network, serve web apps with automatic HTTPS, and containerize everything for consistency. I’ll walk you through setting this up with detailed examples, focusing on practical steps and real-world use cases.
This guide assumes you have basic knowledge of Docker and web servers. Let’s dive into how these tools work together, with complete code examples and tips to make your setup smooth.
Why This Stack? Understanding the Power Trio
Tailscale creates a secure, private VPN network, letting you access your services from anywhere without exposing them to the public internet. Caddy is a modern web server that auto-configures HTTPS and simplifies reverse proxying. Docker ensures your apps run consistently across environments. Together, they make a secure, low-maintenance, and portable dev server.
Here’s a quick overview of their roles:
Tool | Purpose | Key Feature |
---|---|---|
Tailscale | Secure private networking | Zero-config VPN |
Caddy | Web server and reverse proxy | Automatic HTTPS |
Docker | Containerized app deployment | Consistent environments |
This stack is perfect for local development, homelabs, or small-scale production. Let’s set it up step by step.
Setting Up Tailscale for Secure Access
Tailscale creates a private mesh network using WireGuard, letting you access your server securely from any device. No need to open ports or mess with firewall rules.
Installation
Install Tailscale on your server (e.g., Ubuntu). Run this command:
# Install Tailscale
curl -fsSL https://tailscale.com/install.sh | sh
After installation, start Tailscale and authenticate:
# Start Tailscale and log in
sudo tailscale up
# Output: Follow the browser link to authenticate
Follow the link, sign in with your Tailscale account (Google, GitHub, etc.), and your server joins your private network. You’ll get a Tailscale IP (e.g., 100.x.x.x
).
Configuring Access
In the Tailscale admin console (login.tailscale.com), you can manage which devices access your network. For this setup, ensure your server and client devices (e.g., your laptop) are in the same Tailnet.
Tip: Use Tailscale’s MagicDNS to access your server by a hostname (e.g., my-server.ts.net
) instead of an IP. Enable it in the admin console under DNS settings.
Installing Docker for Containerized Apps
Docker lets you run apps in containers, ensuring they work the same everywhere. If Docker isn’t installed, here’s how to set it up on Ubuntu:
# Install Docker
sudo apt update
sudo apt install -y docker.io
sudo systemctl start docker
sudo systemctl enable docker
# Add your user to the Docker group
sudo usermod -aG docker $USER
# Output: Log out and back in for group changes to take effect
Verify Docker is running:
docker --version
# Output: Docker version 20.x.x, build xxxxxxx
Now, let’s create a simple Node.js app to test our setup. Create a directory and add these files:
mkdir my-app && cd my-app
server.js
:
const http = require('http');
const server = http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello from Docker!\n');
});
server.listen(3000, () => {
console.log('Server running on port 3000');
});
Dockerfile
:
FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
package.json
:
{
"name": "my-app",
"version": "1.0.0",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {}
}
Build and run the container:
docker build -t my-app .
docker run -d -p 3000:3000 my-app
# Output: Container ID (e.g., a1b2c3d4e5f6)
Your app is now running on localhost:3000
. You can test it locally with curl localhost:3000
.
Configuring Caddy as a Reverse Proxy
Caddy simplifies serving apps with automatic HTTPS and reverse proxying. Install Caddy on your server:
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update
sudo apt install caddy
Create a Caddyfile
to proxy requests to your Docker app:
touch Caddyfile
Caddyfile
:
my-server.ts.net {
reverse_proxy localhost:3000
}
Start Caddy:
sudo caddy run --config Caddyfile
# Output: Caddy starts, serving at https://my-server.ts.net
Caddy automatically handles HTTPS using Let’s Encrypt, assuming my-server.ts.net
is accessible via Tailscale’s MagicDNS. Access your app at https://my-server.ts.net
from any device in your Tailnet.
Tip: If you’re testing locally, ensure your client device is connected to Tailscale.
Connecting the Pieces: Tailscale + Caddy + Docker
Now, let’s tie everything together. Your Docker app runs on port 3000, Caddy proxies requests from my-server.ts.net
to localhost:3000
, and Tailscale secures access to my-server.ts.net
.
Here’s the flow:
-
Client device (in Tailnet) requests
https://my-server.ts.net
. - Tailscale routes the request to your server’s Tailscale IP.
- Caddy receives the request, handles HTTPS, and proxies it to the Docker container.
- Docker serves the app response.
To verify, connect to your Tailnet and run:
curl https://my-server.ts.net
# Output: Hello from Docker!
If you see the output, everything’s working!
Scaling with Docker Compose
For multiple apps, Docker Compose simplifies managing containers. Let’s add a second app (e.g., a simple Python Flask app) and proxy both apps through Caddy.
Create a docker-compose.yml
:
version: '3.8'
services:
node-app:
build: ./node-app
ports:
- "3000:3000"
flask-app:
build: ./flask-app
ports:
- "5000:5000"
Create a flask-app
directory with these files:
flask-app/app.py
:
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return 'Hello from Flask!'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
flask-app/Dockerfile
:
FROM python:3.9
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 5000
CMD ["python", "app.py"]
flask-app/requirements.txt
:
Flask==2.0.1
Update the Caddyfile
to proxy both apps:
my-server.ts.net {
reverse_proxy /node* localhost:3000
reverse_proxy /flask* localhost:5000
}
Start everything:
docker-compose up -d
sudo caddy run --config Caddyfile
Test the apps:
curl https://my-server.ts.net/node
# Output: Hello from Docker!
curl https://my-server.ts.net/flask
# Output: Hello from Flask!
This setup lets you scale to multiple apps while keeping Caddy’s reverse proxy and Tailscale’s security.
Securing Your Setup Further
To make your server bulletproof, consider these steps:
-
Tailscale ACLs: In the Tailscale admin console, set Access Control Lists (ACLs) to restrict which devices can access
my-server.ts.net
. Example ACL:
{
"acls": [
{ "action": "accept", "src": ["user@example.com"], "dst": ["my-server:*"] }
]
}
- Caddy Security: Enable Caddy’s basic auth for sensitive endpoints:
my-server.ts.net/admin {
basicauth {
user $2a$12$hashedpassword
}
reverse_proxy localhost:3000
}
Generate the hashed password with caddy hash-password
.
-
Docker Best Practices: Run containers as non-root users and limit their resources in
docker-compose.yml
:
node-app:
user: "1000:1000"
deploy:
resources:
limits:
memory: 512M
Troubleshooting Common Issues
Here are common problems and fixes:
Issue | Fix |
---|---|
Tailscale connection fails | Check tailscale status and ensure devices are in the same Tailnet. |
Caddy HTTPS fails | Verify MagicDNS is enabled and my-server.ts.net resolves correctly. |
Docker container crashes | Check logs with docker logs <container_id> . |
For deeper debugging, Tailscale’s support docs are a great resource.
What’s Next? Expanding Your Setup
This stack is a solid foundation for dev servers, but you can take it further:
- Add Monitoring: Run a Prometheus container and proxy it through Caddy for metrics.
- Automate with CI/CD: Use GitHub Actions to build and deploy Docker images.
- Scale with Kubernetes: Migrate your Docker Compose setup to a Kubernetes cluster for production.
Experiment with this setup in your homelab or dev environment. Tailscale keeps it secure, Caddy makes it accessible, and Docker ensures portability. If you hit any snags, the communities on Tailscale, Caddy, and Docker are super helpful.
Top comments (0)
Some comments may only be visible to logged-in visitors. Sign in to view all comments.