This guide explains how to run multiple isolated development environments simultaneously using Docker and DNS-based routing. This is ideal for:
┌─────────────────────────────────────────────────────────────────┐
│ Browser: http://feature-auth.guava.local │
└─────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────────┐
│ Local DNS: *.guava.local → 127.0.0.1 │
│ (dnsmasq on macOS) │
└─────────────────────────┬───────────────────────────────────────┘
│
┌─────────────────────────▼───────────────────────────────────────┐
│ Traefik Reverse Proxy (shared, always running) │
│ - Listens on port 80 │
│ - Routes requests to correct worktree by hostname │
│ - Dashboard at http://localhost:8080 │
└─────────────────────────┬───────────────────────────────────────┘
│
┌────────────────┴────────────────┐
▼ ▼
┌─────────────────────┐ ┌─────────────────────┐
│ feature-auth │ │ fix-bug │
│ ├─ frontend (Vite) │ │ ├─ frontend (Vite) │
│ ├─ backend (Go+air) │ │ ├─ backend (Go+air) │
│ ├─ solver (Python) │ │ ├─ solver (Python) │
│ └─ db (PostgreSQL) │ │ └─ db (PostgreSQL) │
└─────────────────────┘ └─────────────────────┘
Isolated network Isolated network
Isolated volume Isolated volume
Each worktree gets: - Isolated database with its
own Docker volume - Isolated containers with unique
names (prefixed by worktree name) - Isolated network
for inter-service communication - DNS-based routing
so you access each via <worktree>.guava.local
dnsmasq provides local DNS resolution for
*.guava.local domains.
# Install dnsmasq
brew install dnsmasq
# Configure wildcard DNS for *.guava.local
echo 'address=/guava.local/127.0.0.1' >> $(brew --prefix)/etc/dnsmasq.conf
# Start dnsmasq service
sudo brew services start dnsmasq
# Tell macOS to use dnsmasq for .guava.local domains
sudo mkdir -p /etc/resolver
echo "nameserver 127.0.0.1" | sudo tee /etc/resolver/guava.localVerify DNS is working:
# Should return 127.0.0.1
ping -c 1 test.guava.localTraefik runs as a shared service and routes traffic to the correct worktree’s containers.
# From any worktree
make traefik-upTraefik dashboard is available at http://localhost:8080
Note: Keep Traefik running. You only need to start it once; it will auto-discover containers from all worktrees.
# In your worktree directory
make dev-upThis starts: - PostgreSQL database (with isolated volume) - Go backend with hot reload (via air) - React frontend with HMR (via Vite) - Python solver service
Access your app at:
http://<worktree-name>.guava.local
The worktree name is extracted from your directory name. For
example: - Directory la-paz-v1 → URL
http://la-paz-v1.guava.local - Directory
feature-auth → URL
http://feature-auth.guava.local
make dev-up # Start the environment
make dev-status # Show URL and container status
make dev-down # Stop the environment
make dev-logs # Tail logs from all services
make dev-shell # Open shell in backend container
make dev-seed # Seed database with test data
make dev-generate # Run code generation (after proto/sql changes)You can run multiple worktrees simultaneously:
# Terminal 1: In /workspaces/guava/feature-auth
make dev-up
# → http://feature-auth.guava.local
# Terminal 2: In /workspaces/guava/fix-bug
make dev-up
# → http://fix-bug.guava.localBoth environments run independently with their own databases and services.
The Makefile extracts a DNS-safe worktree name from your directory:
mmeinzer/feature-name style)Examples: - /workspaces/guava/la-paz-v1 →
la-paz-v1 - /workspaces/guava/feature-auth →
feature-auth -
/workspaces/guava/UPPERCASE-Name →
uppercase-name
All containers are prefixed with
guava-<worktree-name>-: -
guava-feature-auth-db-1 -
guava-feature-auth-backend-1 -
guava-feature-auth-frontend-1
Containers expose themselves to Traefik via Docker labels: -
Host(\- Routes by hostname -PathPrefix(`/api`)- Routes/api/*`
to backend, everything else to frontend
The backend uses air
for hot reload: - Watches cmd/, internal/,
gen/ directories - Watches .go files only -
Automatically rebuilds and restarts on Go file changes
For proto/sql changes, run code generation manually:
make dev-generateThis keeps hot reload fast (~1-2s) by only rebuilding Go code. Proto and SQL changes are less frequent and intentional.
Configuration: .air.toml
Vite’s built-in HMR works out of the box: - Instant updates on component changes - Preserves React state when possible
The solver service does NOT have automatic reload. Restart with:
docker compose -f docker-compose.dev.yml restart solverIf ping test.guava.local doesn’t work:
# Check dnsmasq is running
brew services list | grep dnsmasq
# Restart dnsmasq
sudo brew services restart dnsmasq
# Verify resolver file exists
cat /etc/resolver/guava.local
# Flush DNS cache
sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponderCheck the Traefik dashboard at http://localhost:8080: - Verify your containers appear under “Services” - Check router rules under “HTTP Routers”
If containers don’t appear:
# Ensure traefik network exists
docker network ls | grep traefik
# Check container is connected to traefik network
docker inspect <container-name> | grep -A 10 NetworksIf Traefik can’t start because port 80 is in use:
# Find what's using port 80
sudo lsof -i :80
# Common culprits: Apache, nginx, other Docker containers
# Stop the conflicting service or change Traefik's port in docker/traefik/docker-compose.yml# Clean up and rebuild
make dev-down
docker system prune -f
make dev-up# Check database is healthy
docker compose -f docker-compose.dev.yml ps
# View database logs
docker compose -f docker-compose.dev.yml logs db
# Connect to database directly
docker compose -f docker-compose.dev.yml exec db psql -U postgres -d guava| Aspect | Native (make dev-backend) |
Docker (make dev-up) |
|---|---|---|
| Setup | Install Go, Node, Postgres locally | Install Docker only |
| Isolation | Shared database, ports | Fully isolated |
| Multi-worktree | Port conflicts | Works seamlessly |
| Performance | Fastest | Slightly slower (Docker overhead) |
| Hot reload | watchexec | air (similar speed) |
| Best for | Single worktree, fast iteration | Multiple worktrees, LLM workflows |
Recommendation: Use Docker
(make dev-up) for LLM-powered workflows and
multi-worktree development. Use native (make dev-backend)
for single-worktree work where you need maximum performance.
docker-compose.dev.yml - Per-worktree compose
filedocker/traefik/docker-compose.yml - Shared Traefik
configurationdocker/backend.dev.Dockerfile - Backend dev image
with hot reloadfrontend/Dockerfile.dev - Frontend dev image with
Vite.air.toml - Go hot reload configurationIf you want to remove the local DNS setup:
# Stop dnsmasq service
sudo brew services stop dnsmasq
# Uninstall dnsmasq
brew uninstall dnsmasq# Remove the guava.local resolver
sudo rm /etc/resolver/guava.local
# If no other resolvers exist, remove the directory
# (only do this if the directory is empty)
sudo rmdir /etc/resolver 2>/dev/null || true# Remove dnsmasq config directory (if you want a clean slate for future reinstalls)
rm -rf $(brew --prefix)/etc/dnsmasq.conf
rm -rf $(brew --prefix)/etc/dnsmasq.d/sudo dscacheutil -flushcache
sudo killall -HUP mDNSRespondermake traefik-down
# Remove the traefik network
docker network rm traefik 2>/dev/null || true# This should now fail to resolve
ping -c 1 test.guava.local
# Expected: "ping: cannot resolve test.guava.local: Unknown host"