Setting up a Local DNS Server with dnsmasq, Caddy, and Docker Compose on Raspberry Pi for Development

Setting up a Local DNS Server with dnsmasq, Caddy, and Docker Compose on Raspberry Pi for Development

Disclaimers

  1. This blog post is a continuation of the previous post: Using Docker Compose and Caddy for Local Network Services on Raspberry Pi. It's recommended to read the previous post to understand the context and setup.

  2. 🖐️ I'm not a DNS expert, but I'll try to explain the concepts simply. If you have any suggestions or corrections, please let me know.

Introduction

When developing web applications locally, it's useful to have a system similar to nip.io that allows you to create custom subdomains pointing to specific IP addresses. This blog post will show you how to set up a similar system using a Raspberry Pi, dnsmasq, Caddy server and Docker.

🖐️ It's limited to your local network and the Raspberry Pi you're using.

This setup is useful for development and testing purposes, allowing you to access multiple services via custom subdomains without modifying hosts files on each device.

Prerequisites

  • A Raspberry Pi running on your local network

  • Docker installed on the Raspberry Pi

  • Basic understanding of DNS and Docker

  • SSH access to your Raspberry Pi

  • Create an SSH key pair on your development machine and add the public key to the Raspberry Pi's ~/.ssh/authorized_keys. See Deploying Applications to Raspberry Pi with Docker Compose for more information.

Project Structure

project-root/
│
├── dnsmasq/                # DNS server configuration
│   ├── dnsmasq.Dockerfile  # dnsmasq Dockerfile
│   ├── dnsmasq.conf        # dnsmasq configuration
│   └── hosts               # hosts file for DNS
│
├── caddy/                  # Caddy server configuration
│   ├── caddy.Dockerfile    # caddy Dockerfile
│   └── Caddyfile           # caddy configuration
│
├── web-services/           # Web services configuration
│   ├── compose.yaml        # Docker compose for web services,
│   │                       # caddy and dnsmasq
│   ├── web1/               # First web service
│   │   ├── Dockerfile
│   │   └── ... (web1 source files)
│   │
│   └── web2/               # Second web service
│       ├── Dockerfile
│       └── ... (web2 source files)

You can retrieve the source code of the two web services from the previous blog post: Using Docker Compose and Caddy for Local Network Services on Raspberry Pi.

Main Objective: Running Multiple Web Services with Custom DNS Names

The main objective is to run two web services on your Raspberry Pi and access them using custom DNS names:

where 192.168.8.175 is the IP address of your Raspberry Pi and t1000.local is the local network name of your Pi.

How It Works:

  1. DNS Resolution
  • When you try to access web1.192.168.8.175.t1000.local or web2.192.168.8.175.t1000.local

  • Your computer asks the DNS server (dnsmasq on Raspberry Pi)

  • dnsmasq always returns 192.168.8.175 (Raspberry Pi's IP) for any *.t1000.local domain

  1. Traffic Routing
  • All web traffic goes to the Raspberry Pi (192.168.8.175)

  • Caddy (reverse proxy) receives all HTTP requests

  • Based on the domain name in the request:

    • web1.192.168.8.175.t1000.local → routes to web1 service

    • web2.192.168.8.175.t1000.local → routes to web2 service

Step 1: Setting Up Remote Docker Context

First, we'll set up a remote Docker context to manage our Raspberry Pi deployment from our development machine:

docker context create \
    --docker host=ssh://k33g@t1000.local \
    --description="Remote engine on Raspberry Pi" \
    t1000-remote

# Switch to the remote context
docker context use t1000-remote

Step 2: Setting Up dnsmasq

Create the following files for the dnsmasq configuration (in the /dnsmasq directory):

dnsmasq.conf

# Basic configuration
domain-needed
bogus-priv
no-resolv
no-hosts

# External DNS servers (using Cloudflare)
server=1.1.1.1
server=1.0.0.1

# Configuration for t1000.local domain
address=/t1000.local/192.168.8.175
expand-hosts
domain=t1000.local

# Enable wildcard mode for subdomains
address=/#.t1000.local/192.168.8.175

hosts

127.0.0.1 localhost
192.168.8.175 t1000.local

dnsmasq.Dockerfile

FROM alpine:latest

# Installation of dnsmasq
RUN apk add --no-cache dnsmasq

# Copy configuration files
COPY dnsmasq.conf /etc/dnsmasq.conf
COPY hosts /etc/hosts

# Expose DNS ports
EXPOSE 53/tcp 53/udp

# Start dnsmasq
CMD ["dnsmasq", "-k"]

Step 3: Setting Up Web Services with Caddy

Create the following files for the Caddy configuration (in the /caddy directory):

Caddyfile

{
    auto_https off
    admin off
}

http://web1.192.168.8.175.t1000.local {
    log {
        format console
    }
    reverse_proxy /* web1:80
}

http://web2.192.168.8.175.t1000.local {
    log {
        format console
    }
    reverse_proxy /* web2:80
}

caddy.Dockerfile

FROM caddy:2-alpine
COPY Caddyfile /etc/caddy/Caddyfile

Step 4: Configuring Web Services, Caddy and dnsmasq (the compose file)

Create the following file at the root of your project:

compose.yaml

services:

  web1:
    build:
      context: ./web1
      dockerfile: Dockerfile
    environment:
      - HTTP_PORT=80
    networks:
      - proxy-network

  web2:
    build:
      context: ./web2
      dockerfile: Dockerfile
    environment:
      - HTTP_PORT=80
    networks:
      - proxy-network

  web3:
    build:
      context: ./web3
      dockerfile: Dockerfile
    environment:
      - HTTP_PORT=80
    networks:
      - proxy-network

  caddy:
    build:
      context: ./caddy
      dockerfile: caddy.Dockerfile
    ports:
      - "80:80"
    networks:
      - proxy-network

  dnsmasq:
    build: 
      context: ./dns
      dockerfile: dnsmasq.Dockerfile
    container_name: dnslocal
    ports:
      - "53:53/udp"
      - "53:53/tcp"
    cap_add:
      - NET_ADMIN
    restart: unless-stopped
    network_mode: "host"

networks:
  proxy-network:

Step 5: Configuring your main workstation

To use this DNS setup on your devices(e.g. your laptop, workstation,...), you need to:

  1. Set your Raspberry Pi's IP (192.168.8.175) as the primary DNS server

  2. Keep your usual DNS server as the secondary DNS server

For example, you can configure DNS on macOS like this:

  1. Open System Preferences → Network

  2. Select your active network connection

  3. Click Advanced → DNS

  4. Add 192.168.8.175 as the first DNS server

  5. Add your regular DNS (e.g., 1.1.1.1) as the second server

About Windows, Linux, and other devices, you can find similar settings in the network configuration.

Step 6: Deploying everything

To build, start the dnsmasq service, the Caddy service, and the two web services:

docker compose up -d

use --build to rebuild the image if needed

Once started you can check if the services are running:

docker ps

Testing the Setup

After deploying all services and configuring your DNS:

  1. Test the DNS resolution:

     ping web1.192.168.8.175.t1000.local
     ping web2.192.168.8.175.t1000.local
    

Both should resolve to 192.168.8.175

  1. Access the web services in your browser:

To add a new service

  1. Create a new service (e.g., web3) and add this to the compose.yaml file:
web3:
  build:
    context: ./web3
    dockerfile: Dockerfile
  environment:
    - HTTP_PORT=80
  networks:
    - proxy-network
  1. Add a new entry in the Caddyfile for the new service
http://web3.192.168.8.175.t1000.local {
    log {
        format console
    }
    reverse_proxy /* web3:80
}
  1. Run docker compose up web3 caddy -d --build to deploy the new service and the updated Caddy configuration.

Conclusion

You now have a local DNS server that can handle wildcard subdomains for your development environment. This setup allows you to create and access new services via custom subdomains without modifying your hosts file for each new service.