Isolated Local Development

This guide explains how to run multiple isolated development environments simultaneously using Docker and DNS-based routing. This is ideal for:

Architecture

┌─────────────────────────────────────────────────────────────────┐
│  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

One-Time Setup

1. Install dnsmasq (macOS)

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.local

Verify DNS is working:

# Should return 127.0.0.1
ping -c 1 test.guava.local

2. Start Traefik (once per machine)

Traefik runs as a shared service and routes traffic to the correct worktree’s containers.

# From any worktree
make traefik-up

Traefik 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.

Per-Worktree Usage

Start Development Environment

# In your worktree directory
make dev-up

This 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

Useful Commands

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)

Multiple Worktrees

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.local

Both environments run independently with their own databases and services.

How It Works

Worktree Name Extraction

The Makefile extracts a DNS-safe worktree name from your directory:

  1. Takes the basename of your current directory
  2. Extracts the part after the last slash (handles mmeinzer/feature-name style)
  3. Converts to lowercase
  4. Replaces non-alphanumeric characters with hyphens

Examples: - /workspaces/guava/la-paz-v1la-paz-v1 - /workspaces/guava/feature-authfeature-auth - /workspaces/guava/UPPERCASE-Nameuppercase-name

Container Naming

All containers are prefixed with guava-<worktree-name>-: - guava-feature-auth-db-1 - guava-feature-auth-backend-1 - guava-feature-auth-frontend-1

Traefik Routing

Containers expose themselves to Traefik via Docker labels: - Host(\.guava.local`)- Routes by hostname -PathPrefix(`/api`)- Routes/api/*` to backend, everything else to frontend

Hot Reload

Backend (Go)

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-generate

This keeps hot reload fast (~1-2s) by only rebuilding Go code. Proto and SQL changes are less frequent and intentional.

Configuration: .air.toml

Frontend (React/Vite)

Vite’s built-in HMR works out of the box: - Instant updates on component changes - Preserves React state when possible

Solver (Python)

The solver service does NOT have automatic reload. Restart with:

docker compose -f docker-compose.dev.yml restart solver

Troubleshooting

DNS Not Resolving

If 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 mDNSResponder

Traefik Not Routing

Check 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 Networks

Port 80 Already In Use

If 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

Container Build Fails

# Clean up and rebuild
make dev-down
docker system prune -f
make dev-up

Database Connection Issues

# 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

Comparison: Docker vs Native Development

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.

Files Reference

Uninstalling / Reverting DNS Changes

If you want to remove the local DNS setup:

1. Stop and remove dnsmasq

# Stop dnsmasq service
sudo brew services stop dnsmasq

# Uninstall dnsmasq
brew uninstall dnsmasq

2. Remove the resolver configuration

# 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

3. Remove dnsmasq configuration (optional)

# 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/

4. Flush DNS cache

sudo dscacheutil -flushcache
sudo killall -HUP mDNSResponder

5. Stop Traefik (if running)

make traefik-down

# Remove the traefik network
docker network rm traefik 2>/dev/null || true

Verify removal

# This should now fail to resolve
ping -c 1 test.guava.local
# Expected: "ping: cannot resolve test.guava.local: Unknown host"