"It works on my machine" is the oldest and most persistent problem in software development. Developer A has Python 3.11, Developer B has 3.9. One has PostgreSQL 15 installed locally, the other uses SQLite for testing. The new hire spends two days setting up their environment because the README is outdated. Docker development environments solve this permanently: every developer gets the same OS, the same tools, the same database versions, with a single command.

This guide covers three approaches to Docker-based development environments: VS Code Dev Containers for a full IDE experience inside Docker, Docker Compose for orchestrating development services, and GitHub Codespaces for cloud-based development. We also cover the practical details that make or break the experience: hot reloading, database seeding, debugging, and environment parity with production.

VS Code Dev Containers

Dev Containers (formerly "Remote - Containers") let VS Code run entirely inside a Docker container. Your code, terminal, extensions, and debugging session all run in the container, while VS Code's UI runs on your host.

devcontainer.json Configuration

// .devcontainer/devcontainer.json
{
  "name": "My App Dev Environment",
  "dockerComposeFile": "../docker-compose.dev.yml",
  "service": "app",
  "workspaceFolder": "/workspace",

  // Features add tools to the container
  "features": {
    "ghcr.io/devcontainers/features/go:1": { "version": "1.22" },
    "ghcr.io/devcontainers/features/docker-in-docker:2": {},
    "ghcr.io/devcontainers/features/git:1": {},
    "ghcr.io/devcontainers/features/github-cli:1": {}
  },

  // VS Code settings inside the container
  "customizations": {
    "vscode": {
      "extensions": [
        "golang.go",
        "ms-azuretools.vscode-docker",
        "eamodio.gitlens",
        "esbenp.prettier-vscode",
        "bradlc.vscode-tailwindcss"
      ],
      "settings": {
        "go.toolsManagement.autoUpdate": true,
        "go.lintTool": "golangci-lint",
        "editor.formatOnSave": true,
        "terminal.integrated.defaultProfile.linux": "bash"
      }
    }
  },

  // Lifecycle scripts
  "postCreateCommand": "go mod download && make setup",
  "postStartCommand": "make dev-services",
  "postAttachCommand": "echo 'Dev environment ready!'",

  // Port forwarding
  "forwardPorts": [8080, 5432, 6379],
  "portsAttributes": {
    "8080": { "label": "App", "onAutoForward": "notify" },
    "5432": { "label": "PostgreSQL", "onAutoForward": "silent" },
    "6379": { "label": "Redis", "onAutoForward": "silent" }
  },

  // Environment variables
  "containerEnv": {
    "DATABASE_URL": "postgres://dev:devpass@postgres:5432/devdb?sslmode=disable",
    "REDIS_URL": "redis://redis:6379",
    "ENV": "development"
  },

  // Run as non-root user
  "remoteUser": "vscode"
}

Custom Dev Container Dockerfile

For projects that need specific tools beyond what features provide, use a custom Dockerfile:

# .devcontainer/Dockerfile
FROM mcr.microsoft.com/devcontainers/go:1.22

# Install project-specific tools
RUN apt-get update && apt-get install -y \
    postgresql-client \
    redis-tools \
    protobuf-compiler \
    && rm -rf /var/lib/apt/lists/*

# Install Go tools
RUN go install github.com/a-h/templ/cmd/templ@latest \
    && go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest \
    && go install github.com/air-verse/air@latest

# Install Tailwind CSS standalone
RUN curl -sL https://github.com/tailwindlabs/tailwindcss/releases/latest/download/tailwindcss-linux-x64 \
    -o /usr/local/bin/tailwindcss && chmod +x /usr/local/bin/tailwindcss

WORKDIR /workspace
// .devcontainer/devcontainer.json (using custom Dockerfile)
{
  "name": "Custom Dev Environment",
  "build": {
    "dockerfile": "Dockerfile",
    "context": ".."
  },
  "workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind",
  "workspaceFolder": "/workspace"
}

Docker Compose for Development

A development Compose file differs significantly from production. It needs source code mounting, hot reloading, debug ports, and relaxed resource constraints:

# docker-compose.dev.yml
services:
  app:
    build:
      context: .
      dockerfile: .devcontainer/Dockerfile
    volumes:
      - .:/workspace:cached
      - go-mod-cache:/go/pkg/mod
      - go-build-cache:/root/.cache/go-build
    ports:
      - "8080:8080"   # Application
      - "2345:2345"   # Delve debugger
    environment:
      - DATABASE_URL=postgres://dev:devpass@postgres:5432/devdb?sslmode=disable
      - REDIS_URL=redis://redis:6379
      - ENV=development
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    command: air  # Hot reloading with Air

  postgres:
    image: postgres:16-alpine
    volumes:
      - pgdata:/var/lib/postgresql/data
      - ./scripts/seed.sql:/docker-entrypoint-initdb.d/seed.sql:ro
    environment:
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: devpass
      POSTGRES_DB: devdb
    ports:
      - "5432:5432"
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev -d devdb"]
      interval: 5s
      timeout: 3s
      retries: 5

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5

  mailhog:
    image: mailhog/mailhog
    ports:
      - "1025:1025"  # SMTP
      - "8025:8025"  # Web UI

volumes:
  pgdata:
  go-mod-cache:
  go-build-cache:
Tip: The :cached mount consistency flag significantly improves performance on macOS. Also, mount Go module and build caches as named volumes (go-mod-cache, go-build-cache) to persist downloaded dependencies and compiled packages between container restarts. Without these, every restart triggers a full download and recompile.

Hot Reloading

Hot reloading rebuilds and restarts your application automatically when source files change. For Go, Air is the standard tool:

# .air.toml
root = "."
testdata_dir = "testdata"
tmp_dir = "tmp"

[build]
  args_bin = ["serve"]
  bin = "./tmp/main"
  cmd = "go build -o ./tmp/main ./cmd/myapp"
  delay = 1000
  exclude_dir = ["assets", "tmp", "vendor", "testdata", "node_modules"]
  exclude_file = []
  exclude_regex = ["_test.go", "_templ.go"]
  exclude_unchanged = false
  follow_symlink = false
  full_bin = ""
  include_dir = []
  include_ext = ["go", "templ", "html", "css"]
  kill_delay = "2s"
  log = "build-errors.log"
  send_interrupt = true
  stop_on_error = true

[misc]
  clean_on_exit = false

[screen]
  clear_on_rebuild = true

For frontend assets with Tailwind CSS, run the watcher alongside Air:

# Makefile targets for development
.PHONY: dev
dev:
	@echo "Starting development environment..."
	docker compose -f docker-compose.dev.yml up -d postgres redis
	@sleep 3
	make dev-watch

.PHONY: dev-watch
dev-watch:
	@# Run Air and Tailwind CSS watcher in parallel
	@air & \
	tailwindcss -i ./web/static/src/input.css -o ./web/static/css/style.css --watch & \
	templ generate --watch & \
	wait

Database Seeding

Development databases need sample data. Place SQL seed files in the init directory for automatic execution on first start:

-- scripts/seed.sql
-- This runs automatically on first container start

-- Create additional schemas
CREATE SCHEMA IF NOT EXISTS app;

-- Seed users
INSERT INTO users (id, email, name, role, created_at) VALUES
  ('550e8400-e29b-41d4-a716-446655440001', '[email protected]', 'Admin User', 'admin', NOW()),
  ('550e8400-e29b-41d4-a716-446655440002', '[email protected]', 'Developer', 'operator', NOW()),
  ('550e8400-e29b-41d4-a716-446655440003', '[email protected]', 'Viewer', 'viewer', NOW())
ON CONFLICT (id) DO NOTHING;

-- Seed sample data
INSERT INTO projects (name, owner_id, description) VALUES
  ('Demo Project', '550e8400-e29b-41d4-a716-446655440001', 'Sample project for development'),
  ('Test Project', '550e8400-e29b-41d4-a716-446655440002', 'Testing environment project')
ON CONFLICT DO NOTHING;

For more complex seeding, use a dedicated script:

#!/bin/bash
# scripts/seed-dev.sh - Reset and seed the development database
set -e

echo "Resetting development database..."
docker exec -i postgres psql -U dev -d devdb -c "DROP SCHEMA public CASCADE; CREATE SCHEMA public;"

echo "Running migrations..."
go run ./cmd/myapp migrate up

echo "Seeding data..."
docker exec -i postgres psql -U dev -d devdb < scripts/seed.sql

echo "Development database ready."

GitHub Codespaces

GitHub Codespaces provides cloud-hosted dev containers. Your .devcontainer/devcontainer.json works identically in Codespaces and local VS Code:

// .devcontainer/devcontainer.json additions for Codespaces
{
  "hostRequirements": {
    "cpus": 4,
    "memory": "8gb",
    "storage": "32gb"
  },

  // Prebuild configuration for faster startup
  "updateContentCommand": "go mod download",
  "postCreateCommand": "make setup && make migrate",

  // Codespaces-specific secrets
  "secrets": {
    "API_KEY": {
      "description": "API key for external service integration"
    }
  }
}

Enable prebuilds for near-instant Codespace creation:

# .github/workflows/codespaces-prebuild.yml
name: Codespaces Prebuild
on:
  push:
    branches: [main]
  schedule:
    - cron: '0 0 * * *'  # Daily

jobs:
  prebuild:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: github/codespaces-precache@v1

Debugging in Containers

For Go applications, use Delve for remote debugging inside Docker:

# Dockerfile.debug
FROM golang:1.22-alpine
RUN go install github.com/go-delve/delve/cmd/dlv@latest
WORKDIR /workspace
COPY . .
RUN go build -gcflags="all=-N -l" -o /app ./cmd/myapp
EXPOSE 8080 2345
CMD ["dlv", "--listen=:2345", "--headless=true", "--api-version=2", "--accept-multiclient", "exec", "/app"]
// .vscode/launch.json
{
  "version": "0.2.0",
  "configurations": [
    {
      "name": "Attach to Docker",
      "type": "go",
      "request": "attach",
      "mode": "remote",
      "remotePath": "/workspace",
      "port": 2345,
      "host": "127.0.0.1"
    }
  ]
}

Environment Parity

The value of Docker development environments is environment parity: dev, test, and production use the same database versions, the same OS, and the same configuration. Here is how to achieve it:

Aspect Development Production Shared
Database version postgres:16-alpine postgres:16-alpine Same image tag
Redis version redis:7-alpine redis:7-alpine Same image tag
Application build Source mount + Air Multi-stage Docker build Same Go version, same deps
Config .env.development .env.production (secrets) Same config structure
Networking Docker bridge Overlay / host Same service names
Warning: Never use latest tags in development Compose files if you pin specific versions in production. A developer running postgres:latest while production runs postgres:16 will eventually encounter version-specific behavior differences that cause production bugs. Pin the same version everywhere.

Maintaining environment parity becomes easier when you use container management platforms like usulnet. You can track which image versions are running in production and mirror those exact versions in your development Compose files, ensuring that "it works on my machine" also means "it will work in production."

Setup Checklist

  1. devcontainer.json committed to repo - New developers run one command to start
  2. Docker Compose with health checks - Services start in the correct order
  3. Named volumes for caches - Dependencies persist between restarts
  4. Hot reloading configured - Changes reflect immediately without manual rebuilds
  5. Database seeding automated - Dev database has useful sample data from first start
  6. Debugging configured - Breakpoints work through Docker without extra setup
  7. Version parity with production - Same database, same Redis, same OS base
  8. Documentation minimal - If it needs a README longer than 5 lines, it needs more automation