Setting up a Local DNS Server with dnsmasq, Caddy, and Docker Compose on Raspberry Pi for Development
Disclaimers
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.
🖐️ 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:
- DNS Resolution
When you try to access
web1.192.168.8.175.t1000.local
orweb2.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
- 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 serviceweb2.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:
Set your Raspberry Pi's IP (192.168.8.175) as the primary DNS server
Keep your usual DNS server as the secondary DNS server
For example, you can configure DNS on macOS like this:
Open System Preferences → Network
Select your active network connection
Click Advanced → DNS
Add 192.168.8.175 as the first DNS server
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:
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
Access the web services in your browser:
To add a new service
- Create a new service (e.g.,
web3
) and add this to thecompose.yaml
file:
web3:
build:
context: ./web3
dockerfile: Dockerfile
environment:
- HTTP_PORT=80
networks:
- proxy-network
- 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
}
- 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.