Software Engineering Ruby on Rails Web Development / August 4, 2025 / 3 mins read / By Wagner Matos

Local Development with Multiple `.test` Domains and Custom Ports using Nginx + dnsmasq

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.