Introduction
When working on multiple local projects, each running on different ports, it becomes inconvenient to remember port numbers and manually update /etc/hosts
for every new project.
Our goal was to make development feel like production — where each project is accessed via its own domain — while keeping the setup flexible, simple, and easy to maintain.
We decided to use .test
domains for local development and route them through Nginx to the correct backend ports without using HTTPS for now.
This makes the setup lightweight and avoids complexity with certificates and SSL configuration in development.
Objective
1 Custom domains like:
http://project1.test
http://project2.test
Without editing /etc/hosts
for every project.
2 Automatic port mapping so each domain routes to the correct local project.
3 Wildcard DNS so any .test
domain resolves to 127.0.0.1
.
4 Simple and maintainable — easy to add new projects without rewriting lots of Nginx blocks.
Step 1 — Choosing .test
instead of .local
Originally, I’ve considered .local
for domains (project1.local
), but .local
is reserved for mDNS (Bonjour) and doesn’t work reliably with DNS overrides.
I switched to .test
because:
- It’s officially reserved for testing by ICANN.
- It doesn’t conflict with any real domains.
- Browsers treat it as a normal domain.
Step 2 — Wildcard DNS with dnsmasq
I used dnsmasq
to resolve any .test
domain to 127.0.0.1
without editing /etc/hosts
.
Install and configure dnsmasq:
brew install dnsmasq
echo 'address=/.test/127.0.0.1' > $(brew --prefix)/etc/dnsmasq.conf
sudo brew services start dnsmasq
Tell macOS to use dnsmasq for .test lookups:
sudo mkdir -p /etc/resolver
echo "nameserver 127.0.0.1" | sudo tee /etc/resolver/test
Test
dig @127.0.0.1 project1.test
It should return 127.0.0.1.
.
Step 3 - Nginx as a Reverse Proxy
I wanted:
- One central Nginx instalce listening on port 80.
- Requests to
projectname.test
pointing to the correct backend - Avoid multiple, repetitive server blocks
I used Nginx map directive to create a hostname -> port mapping.
Step 4 - installing Nginx and Fixing configuration conflicts
brew install nginx
I found that nginx.conf
included in the same directory twice:
include servers/*.conf;
include servers/*;
This cause an issue with conflicting server name
warnings. I just removed one line and the warning was gone.
Step 5 - Final Nginx Config (HTTP Only)
Initially I considered adding HTTPS to the projects but at the end I decided against it as I didn’t think the overhead was warranted for current projects. I may revisit in the future.
Here’s the final Nginx config file:
# Map hostnames to backend ports
map $host $backend {
default "127.0.0.1:3000"; # fallback
project1.test "127.0.0.1:3033";
project2.test "127.0.0.1:8000";
project3.test "127.0.0.1:3039";
project4.test "127.0.0.1:1313";
}
# HTTP for all domains
server {
listen 80;
server_name ~^(.+)\.test$;
location / {
proxy_pass http://$backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Step 6 - Testing
Start the respective backend servers and visited each one. Each worked as expected.
Why this approach works well for me
- No editing /etc/hosts — dnsmasq + /etc/resolver handles wildcard DNS.
- Easy to add projects — just add a new line in the map block.
- Simple setup — no SSL certificates to manage.
- Matches production routing — subdomains and central proxy handling.
- Avoids conflicts — single include path in nginx.conf.