User-Specific Hosts File Alternative: Creating ~/.hosts Equivalent for Local DNS Overrides


1 views

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:

  1. Check DNS resolution order (nsswitch.conf on Linux)
  2. Verify no system-wide DNS overrides exist
  3. Test with dig/nslookup before application-level testing
  4. Ensure no caching (dscacheutil -flushcache on MacOS)