DEV Community

Shrijith Venkatramana
Shrijith Venkatramana

Posted on

2 1 1 2 1

Your Ultimate Dev Server Setup: With Tailscale, Caddy, and Docker

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
Enter fullscreen mode Exit fullscreen mode

After installation, start Tailscale and authenticate:

# Start Tailscale and log in
sudo tailscale up
# Output: Follow the browser link to authenticate
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Verify Docker is running:

docker --version
# Output: Docker version 20.x.x, build xxxxxxx
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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');
});
Enter fullscreen mode Exit fullscreen mode

Dockerfile:

FROM node:18
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

package.json:

{
  "name": "my-app",
  "version": "1.0.0",
  "main": "server.js",
  "scripts": {
    "start": "node server.js"
  },
  "dependencies": {}
}
Enter fullscreen mode Exit fullscreen mode

Build and run the container:

docker build -t my-app .
docker run -d -p 3000:3000 my-app
# Output: Container ID (e.g., a1b2c3d4e5f6)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Create a Caddyfile to proxy requests to your Docker app:

touch Caddyfile
Enter fullscreen mode Exit fullscreen mode

Caddyfile:

my-server.ts.net {
  reverse_proxy localhost:3000
}
Enter fullscreen mode Exit fullscreen mode

Start Caddy:

sudo caddy run --config Caddyfile
# Output: Caddy starts, serving at https://my-server.ts.net
Enter fullscreen mode Exit fullscreen mode

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:

  1. Client device (in Tailnet) requests https://my-server.ts.net.
  2. Tailscale routes the request to your server’s Tailscale IP.
  3. Caddy receives the request, handles HTTPS, and proxies it to the Docker container.
  4. Docker serves the app response.

To verify, connect to your Tailnet and run:

curl https://my-server.ts.net
# Output: Hello from Docker!
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

flask-app/requirements.txt:

Flask==2.0.1
Enter fullscreen mode Exit fullscreen mode

Update the Caddyfile to proxy both apps:

my-server.ts.net {
  reverse_proxy /node* localhost:3000
  reverse_proxy /flask* localhost:5000
}
Enter fullscreen mode Exit fullscreen mode

Start everything:

docker-compose up -d
sudo caddy run --config Caddyfile
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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:*"] }
    ]
  }
Enter fullscreen mode Exit fullscreen mode
  • Caddy Security: Enable Caddy’s basic auth for sensitive endpoints:
  my-server.ts.net/admin {
    basicauth {
      user $2a$12$hashedpassword
    }
    reverse_proxy localhost:3000
  }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

Tiger Data image

🐯 🚀 Timescale is now TigerData

Building the modern PostgreSQL for the analytical and agentic era.

Read more

Top comments (0)

Some comments may only be visible to logged-in visitors. Sign in to view all comments.

Tiger Data image

🐯 🚀 Timescale is now TigerData: Building the Modern PostgreSQL for the Analytical and Agentic Era

We’ve quietly evolved from a time-series database into the modern PostgreSQL for today’s and tomorrow’s computing, built for performance, scale, and the agentic future.

So we’re changing our name: from Timescale to TigerData. Not to change who we are, but to reflect who we’ve become. TigerData is bold, fast, and built to power the next era of software.

Read more

👋 Kindness is contagious

Explore this insightful write-up, celebrated by our thriving DEV Community. Developers everywhere are invited to contribute and elevate our shared expertise.

A simple "thank you" can brighten someone’s day—leave your appreciation in the comments!

On DEV, knowledge-sharing fuels our progress and strengthens our community ties. Found this useful? A quick thank you to the author makes all the difference.

Okay