| name | local-dev-environment |
| description | Set up, configure, or troubleshoot local development environments using Docker Compose, Dev Containers, Tilt, Skaffold, task runners, and environment variable management. |
Local Development Environment
A consistent, reproducible local dev environment eliminates "works on my machine" problems, reduces onboarding time, and mirrors production closely enough to catch integration issues early.
+----------------------------------------------------------+
| Developer Machine |
| |
| +----------------+ +---------------------------+ |
| | Task Runner | | Dev Container / Codespace | |
| | make dev |--->| postCreateCommand | |
| | task migrate | | VS Code Extensions | |
| +----------------+ +---------------------------+ |
| | | |
| v v |
| +-------------------------------------------+ |
| | Docker Compose | |
| | +----------+ +-------+ +-----------+ | |
| | |PostgreSQL| | Redis | | Kafka | | |
| | | :5432 | | :6379 | | :9092 | | |
| | | (volume) | |(vol.) | | (vol.) | | |
| | +----------+ +-------+ +-----------+ | |
| | App :8080 | |
| +-------------------------------------------+ |
| | |
| direnv: auto-load .envrc on cd |
| .env: local overrides (never committed) |
+----------------------------------------------------------+
Docker Compose for Local Dev
Docker Compose is the standard way to model local infrastructure dependencies. The goal is one command (docker compose up) to start everything the app needs.
Full docker-compose.yml Example
PostgreSQL + Redis + Kafka + Zookeeper + app with health checks, volumes, and profiles.
version: "3.9"
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: ${DB_USER:-appuser}
POSTGRES_PASSWORD: ${DB_PASSWORD:-secret}
POSTGRES_DB: ${DB_NAME:-appdb}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./db/init:/docker-entrypoint-initdb.d
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-appuser} -d ${DB_NAME:-appdb}"]
interval: 5s
timeout: 5s
retries: 10
start_period: 10s
profiles: ["infra", "all"]
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
ports:
- "6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
profiles: ["infra", "all"]
zookeeper:
image: confluentinc/cp-zookeeper:7.6.0
environment:
ZOOKEEPER_CLIENT_PORT: 2181
ZOOKEEPER_TICK_TIME: 2000
ports:
- "2181:2181"
volumes:
- zk_data:/var/lib/zookeeper/data
- zk_logs:/var/lib/zookeeper/log
healthcheck:
test: ["CMD", "bash", "-c", "echo ruok | nc localhost 2181 | grep imok"]
interval: 10s
timeout: 5s
retries: 5
profiles: ["infra", "all"]
kafka:
image: confluentinc/cp-kafka:7.6.0
depends_on:
zookeeper:
condition: service_healthy
ports:
- "9092:9092"
- "29092:29092"
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: PLAINTEXT:PLAINTEXT,PLAINTEXT_HOST:PLAINTEXT
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:29092,PLAINTEXT_HOST://localhost:9092
KAFKA_INTER_BROKER_LISTENER_NAME: PLAINTEXT
KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
KAFKA_AUTO_CREATE_TOPICS_ENABLE: "true"
volumes:
- kafka_data:/var/lib/kafka/data
healthcheck:
test: ["CMD", "kafka-broker-api-versions", "--bootstrap-server", "localhost:29092"]
interval: 10s
timeout: 10s
retries: 10
start_period: 30s
profiles: ["infra", "all"]
app:
build:
context: .
dockerfile: Dockerfile.dev
target: dev
ports:
- "8080:8080"
- "5005:5005"
volumes:
- .:/workspace:cached
- app_gradle:/root/.gradle
environment:
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/${DB_NAME:-appdb}
SPRING_DATASOURCE_USERNAME: ${DB_USER:-appuser}
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD:-secret}
SPRING_REDIS_HOST: redis
SPRING_KAFKA_BOOTSTRAP_SERVERS: kafka:29092
JAVA_TOOL_OPTIONS: "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
kafka:
condition: service_healthy
profiles: ["all"]
volumes:
postgres_data:
redis_data:
zk_data:
zk_logs:
kafka_data:
app_gradle:
Key Patterns
Health checks before app starts
depends_on:
postgres:
condition: service_healthy
Persist data between restarts
volumes:
- postgres_data:/var/lib/postgresql/data
- .:/workspace:cached
Profiles — start only infra
docker compose --profile infra up -d
docker compose --profile all up
docker compose --profile all down
docker compose --profile all down -v
.env file for local overrides
DB_USER=myuser
DB_PASSWORD=localpassword123
DB_NAME=myapp_dev
DB_USER=appuser
DB_PASSWORD=changeme
DB_NAME=appdb
.gitignore additions
.env
.env.local
*.env
!.env.example
docker compose watch (Compose 2.22+)
Sync file changes into container without rebuild — sub-second feedback for interpreted languages.
develop:
watch:
- action: sync
path: ./src
target: /workspace/src
ignore:
- node_modules/
- action: rebuild
path: package.json
docker compose watch
Anti-Patterns
- No health checks — app starts before DB is ready; random connection errors on first boot
- Root user in postgres — use a dedicated app user; matches production
- No volume for DB data — data lost on every
compose down; migrations re-run constantly
- Hard-coded credentials in compose file — use environment variable substitution with
.env
latest image tag — breaks reproducibility; pin to a specific version like postgres:16-alpine
Dev Containers
A Dev Container runs the entire development environment inside a Docker container. Every team member gets an identical environment; no "install this first" setup docs needed.
+---------------------------------------------+
| Host Machine |
| +---------------------------------------+ |
| | VS Code / JetBrains / GitHub Codespace| |
| | +----------------------------------+ | |
| | | Dev Container | | |
| | | Java 21, Maven, Node 20 | | |
| | | VS Code Extensions installed | | |
| | | postCreateCommand: mvn install | | |
| | | /workspace -> source code | | |
| | +----------------------------------+ | |
| | | | |
| | Port Forwards: 8080, 5005, 9092 | |
| +---------------------------------------+ |
+---------------------------------------------+
Full devcontainer.json for Spring Boot + Kafka
{
"name": "Spring Boot + Kafka Dev",
"dockerComposeFile": ["../docker-compose.yml"],
"service": "app",
"workspaceFolder": "/workspace",
"features": {
"ghcr.io/devcontainers/features/java:1": {
"version": "21",
"jdkDistro": "ms"
},
"ghcr.io/devcontainers/features/node:1": {
"version": "20"
},
"ghcr.io/devcontainers/features/docker-in-docker:2": {
"version": "latest"
},
"ghcr.io/devcontainers/features/kubectl-helm-minikube:1": {
"version": "latest",
"helm": "latest",
"minikube": "none"
}
},
"customizations": {
"vscode": {
"extensions": [
"vscjava.vscode-java-pack",
"vmware.vscode-spring-boot",
"redhat.java",
"redhat.vscode-yaml",
"ms-azuretools.vscode-docker",
"eamodio.gitlens",
"usernamehw.errorlens",
"sonarsource.sonarlint-vscode"
],
"settings": {
"java.jdt.ls.java.home": "/usr/local/sdkman/candidates/java/current",
"editor.formatOnSave": true,
"editor.defaultFormatter": "redhat.java"
}
}
},
"forwardPorts": [8080, 5005, 9092, 5432, 6379],
"portsAttributes": {
"8080": { "label": "App", "onAutoForward": "notify" },
"5005": { "label": "JVM Debug", "onAutoForward": "silent" },
"9092": { "label": "Kafka", "onAutoForward": "silent" }
},
"postCreateCommand": "mvn dependency:go-offline -q && echo 'Dependencies cached'",
"postStartCommand": "docker compose --profile infra up -d",
"remoteUser": "vscode",
"runServices": ["postgres", "redis", "zookeeper", "kafka"]
}
Key Fields
| Field | Purpose |
|---|
features | Add tools without custom Dockerfile; versioned and composable |
postCreateCommand | Runs once after container created; install deps, compile |
postStartCommand | Runs every time container starts; start infra services |
forwardPorts | Ports available on host; click to open in browser |
runServices | Which compose services start with the dev container |
Using Features Without Custom Dockerfile
"features": {
"ghcr.io/devcontainers/features/python:1": { "version": "3.12" },
"ghcr.io/devcontainers/features/terraform:1": { "version": "latest" },
"ghcr.io/devcontainers/features/aws-cli:1": {}
}
GitHub Codespaces
Same devcontainer.json works in Codespaces. Open any PR in a pre-configured environment:
gh codespace create --repo myorg/myapp --branch feature/my-feature
gh codespace list
gh codespace ssh
Tilt for Kubernetes Local Dev
Tilt watches your source code and keeps your Kubernetes local cluster in sync automatically.
+--------------------------------------------------+
| Tilt Dev Loop |
| |
| Source Change |
| | |
| v |
| docker_build() --> push to local registry |
| | |
| v |
| Live Update: copy files into pod (sub-second) |
| OR |
| k8s_yaml(): apply manifest changes |
| | |
| v |
| Tilt UI (localhost:10350) |
| - Build status per service |
| - Logs streamed in real time |
| - Port forwards clickable |
+--------------------------------------------------+
Tiltfile
load('ext://helm_resource', 'helm_resource', 'helm_repo')
load('ext://namespace', 'namespace_create')
namespace_create('dev')
docker_build(
'myorg/myapp',
context='.',
dockerfile='Dockerfile',
only=['src/', 'pom.xml'],
live_update=[
sync('./target/classes', '/app/classes'),
run('touch /app/restart.txt', trigger=['./target/classes']),
],
ignore=['target/', '.git/', '**/*.md']
)
k8s_yaml(kustomize('./k8s/overlays/local'))
k8s_resource(
'myapp',
port_forwards=['8080:8080', '5005:5005'],
resource_deps=['postgres', 'redis'],
labels=['app']
)
helm_repo('bitnami', 'https://charts.bitnami.com/bitnami')
helm_resource(
'postgres',
'bitnami/postgresql',
namespace='dev',
flags=['--set', 'auth.password=secret', '--set', 'auth.database=appdb'],
port_forwards=['5432:5432'],
labels=['infra']
)
helm_resource(
'redis',
'bitnami/redis',
namespace='dev',
flags=['--set', 'auth.enabled=false'],
port_forwards=['6379:6379'],
labels=['infra']
)
Key Commands
tilt up
tilt down
tilt ci
tilt args -- --env dev
Skaffold
Skaffold is Google's alternative to Tilt; integrates tightly with Helm and Kustomize.
apiVersion: skaffold/v4beta6
kind: Config
metadata:
name: myapp
build:
artifacts:
- image: myorg/myapp
jib:
project: com.myorg:myapp
args:
- -Plocal
local:
push: false
useBuildkit: true
deploy:
helm:
releases:
- name: myapp
chartPath: ./helm/myapp
valuesFiles:
- ./helm/myapp/values-local.yaml
setValues:
image.repository: myorg/myapp
image.tag: ""
portForward:
- resourceType: service
resourceName: myapp
namespace: dev
port: 8080
localPort: 8080
- resourceType: service
resourceName: myapp
namespace: dev
port: 5005
localPort: 5005
profiles:
- name: local
activation:
- kubeContext: docker-desktop
patches:
- op: replace
path: /build/local/push
value: false
- name: ci
patches:
- op: replace
path: /build/local/push
value: true
skaffold dev
skaffold dev --profile=local
skaffold run
skaffold delete
Task Runners
Task runners codify common development commands. make is universally available; Task (taskfile.dev) is more ergonomic for complex workflows.
Makefile
.PHONY: dev test lint migrate seed clean build docker-build help
COMPOSE=docker compose --profile all
INFRA=docker compose --profile infra
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?
dev: ## Start full local environment (infra + app with hot reload)
$(INFRA) up -d
./gradlew bootRun --args='--spring.profiles.active=local'
infra: ## Start only infrastructure services
$(INFRA) up -d
test: ## Run all tests
./gradlew test
test-unit: ## Run unit tests only
./gradlew test --tests '*Unit*'
test-integration: ## Run integration tests (requires infra running)
$(INFRA) up -d
./gradlew integrationTest
lint: ## Run linters and static analysis
./gradlew spotlessCheck checkstyleMain
lint-fix: ## Auto-fix lint issues
./gradlew spotlessApply
migrate: ## Run database migrations
$(INFRA) up -d postgres
./gradlew flywayMigrate
seed: ## Seed database with test data
$(INFRA) up -d postgres
./gradlew bootRun --args='--spring.profiles.active=seed' 2>&1 | grep -E 'Seeded|ERROR'
build: ## Build the application
./gradlew build -x test
docker-build: build ## Build Docker image
docker build -t myorg/myapp:local .
clean: ## Stop all services and remove containers
$(COMPOSE) down -v
./gradlew clean
logs: ## Tail logs from all compose services
$(COMPOSE) logs -f
ps: ## Show status of compose services
$(COMPOSE) ps
Taskfile (YAML, cross-platform)
version: '3'
vars:
APP_NAME: myapp
COMPOSE_CMD: docker compose --profile all
INFRA_CMD: docker compose --profile infra
env:
GRADLE_OPTS: "-Dorg.gradle.daemon=false"
tasks:
default:
desc: Show available tasks
cmds:
- task --list
dev:
desc: Start full local environment with hot reload
cmds:
- task: infra
- ./gradlew bootRun --args='--spring.profiles.active=local'
infra:
desc: Start only infrastructure services (DB, cache, broker)
cmds:
- "{{.INFRA_CMD}} up -d"
status:
- docker compose ps postgres | grep healthy
test:
desc: Run all tests
cmds:
- ./gradlew test
sources:
- src/**/*.java
- build.gradle*
generates:
- build/reports/tests/**
test:unit:
desc: Run unit tests only
cmds:
- ./gradlew test --tests '*Unit*'
test:integration:
desc: Run integration tests (starts infra automatically)
deps: [infra]
cmds:
- ./gradlew integrationTest
lint:
desc: Run linters
cmds:
- ./gradlew spotlessCheck checkstyleMain
lint:fix:
desc: Auto-fix lint issues
cmds:
- ./gradlew spotlessApply
migrate:
desc: Run database migrations
deps: [infra]
cmds:
- ./gradlew flywayMigrate
seed:
desc: Seed database with development data
deps: [migrate]
cmds:
- ./gradlew bootRun --args='--spring.profiles.active=seed'
build:
desc: Build application artifact
cmds:
- ./gradlew build -x test
docker:build:
desc: Build Docker image
deps: [build]
cmds:
- docker build -t myorg/{{.APP_NAME}}:local .
clean:
desc: Stop all services and clean build artifacts
cmds:
- "{{.COMPOSE_CMD}} down -v"
- ./gradlew clean
logs:
desc: Tail logs from all services
cmds:
- "{{.COMPOSE_CMD}} logs -f"
brew install go-task
sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d
task dev
task test
task lint:fix
task --list
Environment Variable Management
direnv — Auto-Load on cd
direnv hooks into your shell and loads .envrc automatically when you enter a directory.
brew install direnv
eval "$(direnv hook zsh)"
export DB_URL="postgresql://appuser:secret@localhost:5432/appdb"
export REDIS_URL="redis://localhost:6379"
export KAFKA_BROKERS="localhost:9092"
export LOG_LEVEL="DEBUG"
export AWS_PROFILE="myapp-dev"
layout python3
direnv allow .
dotenv Libraries
pip install python-dotenv
DB_URL=postgresql://appuser:secret@localhost:5432/appdb
DEBUG=true
from dotenv import load_dotenv
import os
load_dotenv()
db_url = os.getenv("DB_URL")
import "github.com/joho/godotenv"
func init() {
_ = godotenv.Load()
}
require('dotenv').config()
const dbUrl = process.env.DB_URL
Secret Management
eval $(op signin)
export DB_PASSWORD=$(op read "op://dev-vault/postgres/password")
export DB_PASSWORD=$(aws secretsmanager get-secret-value \
--secret-id myapp/dev/db-password \
--query SecretString --output text)
export DB_PASSWORD=$(vault kv get -field=password secret/myapp/dev/postgres)
.env.example Template
DB_HOST=localhost
DB_PORT=5432
DB_NAME=appdb
DB_USER=appuser
DB_PASSWORD=changeme
REDIS_URL=redis://localhost:6379
KAFKA_BROKERS=localhost:9092
LOG_LEVEL=DEBUG
APP_PORT=8080
AWS_PROFILE=myapp-dev
AWS_REGION=us-east-1
Checklist
New Project Setup
Dev Container (Team Uniformity)
Security