While /etc/hosts
works system-wide, modern development workflows often require user-specific DNS overrides. Common scenarios include:
- Testing different environments without affecting other users
- Maintaining personal development configurations
- Running multiple projects with conflicting hostnames
- Temporary domain mappings for local development
Create a user-level hosts file and implement a custom resolver:
# Create the user hosts file
touch ~/.hosts
chmod 600 ~/.hosts
# Sample content format (same as /etc/hosts)
127.0.0.1 myapp.local
::1 test.dev.local
Option 1: Bash Function for Local Development
Add to your ~/.bashrc
or ~/.zshrc
:
function localhosts() {
if [ -f ~/.hosts ]; then
while read -r line; do
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
ip=$(echo $line | awk '{print $1}')
host=$(echo $line | awk '{print $2}')
echo "$ip $host" | sudo tee -a /etc/hosts > /dev/null
done < ~/.hosts
fi
}
function clearlocalhosts() {
if [ -f ~/.hosts ]; then
while read -r line; do
[[ "$line" =~ ^#.*$ || -z "$line" ]] && continue
host=$(echo $line | awk '{print $2}')
sudo sed -i "/$host/d" /etc/hosts
done < ~/.hosts
fi
}
Option 2: Python DNS Proxy
Create a lightweight DNS proxy that checks ~/.hosts
first:
import socket
from dnslib import *
class LocalDNSProxy:
def __init__(self):
self.hosts = self.load_hosts()
def load_hosts(self):
hosts = {}
try:
with open(os.path.expanduser('~/.hosts'), 'r') as f:
for line in f:
if line.startswith('#') or not line.strip():
continue
parts = line.split()
if len(parts) >= 2:
hosts[parts[1]] = parts[0]
except FileNotFoundError:
pass
return hosts
def handle_query(self, data, addr, sock):
request = DNSRecord.parse(data)
qname = str(request.q.qname)
if qname in self.hosts:
reply = DNSRecord(DNSHeader(id=request.header.id, qr=1, aa=1, ra=1), q=request.q)
reply.add_answer(RR(qname, QTYPE.A, rdata=A(self.hosts[qname])))
return reply.pack()
# Forward to system resolver
return None
Systemd-Resolved Integration
For Linux systems using systemd-resolved:
# Create custom DNS configuration
mkdir -p ~/.config/systemd/resolved.conf.d
echo -e "[Resolve]\nDNSOverTLS=opportunistic" > ~/.config/systemd/resolved.conf.d/localhosts.conf
# Create a script to sync ~/.hosts to DNSMasq
cat > ~/bin/sync_hosts.sh << 'EOF'
#!/bin/bash
awk '{print "address=/"$2"/"$1}' ~/.hosts > /tmp/dnsmasq-hosts.conf
sudo mv /tmp/dnsmasq-hosts.conf /etc/dnsmasq.d/local-user-hosts.conf
sudo systemctl restart dnsmasq
EOF
chmod +x ~/bin/sync_hosts.sh
Different OS approaches:
- macOS: Use
scutil
to manage DNS configurations programmatically - Windows: Modify the registry key
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\DataBasePath
- Linux: Use
nsswitch.conf
with custom module or dnsmasq
When implementing user-specific hosts:
- Set strict permissions on
~/.hosts
(600) - Validate entries to prevent DNS spoofing
- Consider using containers for complete isolation
- Document all custom mappings for team awareness
While /etc/hosts serves as the system-wide hosts file in Unix-like systems, many developers need per-user host overrides for testing scenarios without affecting other users. This is particularly useful when:
- Multiple developers share a CI/CD environment
- Testing different staging environments
- Running local development containers
- Working with microservices architecture
Here's how to implement user-specific hosts files on different platforms:
Linux/MacOS: Using dnsmasq
Install and configure dnsmasq for user-specific resolution:
brew install dnsmasq # MacOS sudo apt install dnsmasq # Debian/Ubuntu # Create user config mkdir -p ~/.dnsmasq.d echo "address=/test.local/127.0.0.1" > ~/.dnsmasq.d/local.conf # Run with user config dnsmasq -C ~/.dnsmasq.d/local.conf --no-daemon
Windows: Using Acrylic DNS
For Windows users:
# In Acrylic configuration (acrylic.ini) [LocalHosts] 127.0.0.1 test.local ::1 test.local # Per-user locations supported
Cross-Platform: Node.js Solution
Create a local DNS proxy with Node:
const dns = require('dns'); const http = require('http'); const customHosts = { 'dev.example.com': '127.0.0.1', 'api.local': '192.168.1.100' }; dns.lookup = (hostname, options, callback) => { if (customHosts[hostname]) { return callback(null, customHosts[hostname], 4); } return originalLookup(hostname, options, callback); };
- Use clear naming conventions (user-dev-, user-staging- prefixes)
- Document all overrides in a team wiki
- Consider version controlling user host files
- Implement cleanup scripts to remove temporary entries
For Docker-based workflows:
# docker-compose.yml version: '3' services: dnsmasq: image: andyshinn/dnsmasq volumes: - ./user-hosts:/etc/dnsmasq.d ports: - "53:53/udp"
Store per-user configurations in the user-hosts directory mounted to the container.
When user-specific hosts don't work:
- Check DNS resolution order (nsswitch.conf on Linux)
- Verify no system-wide DNS overrides exist
- Test with dig/nslookup before application-level testing
- Ensure no caching (dscacheutil -flushcache on MacOS)