5 min read
On this page

Local Dev Setup

One command to set up. One command to run. That is the standard. If a new engineer cannot clone the repo and have the application running locally in 30 minutes, your development setup is broken. It does not matter how good your architecture is or how clean your code is if people cannot run it.

Every hour a new hire spends fighting environment setup is an hour they are not shipping code. Worse, it sends a signal: "We do not care about developer experience here." The engineers who are most sensitive to that signal are usually the ones you most want to retain.

The 30-Minute Rule

Time yourself. Clone the repo into a fresh directory. Follow only the written instructions. If you cannot run the project and see it work in 30 minutes, document every obstacle you hit and fix them. This is not a one-time exercise. Do it quarterly, because setups rot as dependencies change and services get added.

What "working" means:
- Application starts without errors
- You can hit the main page or API endpoint
- Authentication works (with test credentials)
- You can make a code change and see it reflected
- Tests pass

If any of those steps require tribal knowledge -- asking a teammate, reading a Slack thread from 6 months ago, manually editing a config file -- that is a bug in your setup process.

The Makefile Pattern

A Makefile (or Justfile, or Taskfile) gives every developer a consistent interface regardless of the underlying tooling. Nobody needs to remember whether it is docker compose up -d && npm run migrate && npm run seed && npm start or some other incantation.

# Makefile

.PHONY: setup run test clean

setup:            ## First-time setup: install deps, create DB, seed data
	cp -n .env.example .env
	docker compose up -d postgres redis
	npm install
	npm run db:migrate
	npm run db:seed
	@echo "Setup complete. Run 'make run' to start the application."

run:              ## Start the application in development mode
	docker compose up -d postgres redis
	npm run dev

test:             ## Run the test suite
	npm test

test-watch:       ## Run tests in watch mode
	npm test -- --watch

clean:            ## Stop services, remove volumes, clean build artifacts
	docker compose down -v
	rm -rf node_modules dist .next

lint:             ## Run linters
	npm run lint

help:             ## Show this help
	@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | \
		awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-15s\033[0m %s\n", $$1, $$2}'

.DEFAULT_GOAL := help

The help target is important. When a developer types make with no arguments, they see every available command with a description. No README hunting, no guessing.

For teams that prefer something more modern than Make, Justfile (from the just command runner) provides a similar interface with better ergonomics:

# Justfile

setup:
    cp -n .env.example .env
    docker compose up -d postgres redis
    npm install
    npm run db:migrate
    npm run db:seed

run:
    docker compose up -d postgres redis
    npm run dev

test *args:
    npm test {{args}}

The exact tool does not matter. What matters is that make setup && make run (or equivalent) takes you from zero to a working application.

Docker Compose for Services

Most applications depend on external services: databases, caches, message queues, search engines. Installing PostgreSQL, Redis, and Elasticsearch locally is a recipe for version conflicts and "it works on my machine" bugs. Docker Compose standardizes the service layer.

# docker-compose.yml
services:
  postgres:
    image: postgres:16
    ports:
      - "5432:5432"
    environment:
      POSTGRES_DB: myapp_dev
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: dev
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: pg_isready -U dev
      interval: 5s
      timeout: 3s
      retries: 5

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

  localstack:
    image: localstack/localstack:3
    ports:
      - "4566:4566"
    environment:
      SERVICES: s3,sqs,ses

volumes:
  postgres_data:

Health checks matter. Without them, your application might start before the database is ready, causing confusing connection errors. With health checks, docker compose up --wait blocks until all services are healthy.

Notice the version pins. postgres:16, not postgres:latest. Latest changes without warning and will eventually break your setup at the worst possible time.

Environment Variables & .env Files

Every project needs a .env.example file checked into the repo with every required variable documented. The setup script copies it to .env (which is gitignored). No developer should have to ask "what environment variables do I need?"

# .env.example

# Database
DATABASE_URL=postgresql://dev:dev@localhost:5432/myapp_dev

# Redis
REDIS_URL=redis://localhost:6379

# Auth (use these test credentials for local development)
JWT_SECRET=local-dev-secret-do-not-use-in-production
OAUTH_CLIENT_ID=dev-client-id
OAUTH_CLIENT_SECRET=dev-client-secret

# External Services (localstack)
AWS_ENDPOINT_URL=http://localhost:4566
AWS_ACCESS_KEY_ID=test
AWS_SECRET_ACCESS_KEY=test
AWS_REGION=us-east-1

# Feature Flags
FEATURE_NEW_CHECKOUT=true

Principles for .env.example:

- Every variable has a working default for local development
- No real secrets or API keys (use dummy values)
- Comments explain what each variable does
- Group related variables with blank lines and headers
- Values that work out of the box with Docker Compose

For variables that require real credentials (third-party API keys), document where to get them and provide a mock or stub for local development. Never make a real API key required to start the application locally.

Seed Data

An empty database is almost as useless as no database. Seed data gives developers something to look at and interact with immediately.

# Good seed data includes:
- A test user with known credentials (test@example.com / password)
- A realistic set of sample data (10-50 records, not 1)
- Data that exercises edge cases (empty fields, long strings, special characters)
- Relationships between entities (orders with items, users with teams)
- Different states (active, archived, pending)

# Bad seed data:
- One record named "test" with all fields set to "test"
- Production data dump (slow, privacy concerns, stale)
- Nothing (forces every developer to create data manually)

Seed data should be idempotent. Running it twice should not create duplicates or error out. Use upserts or check-before-insert patterns.

# seed.sql example (PostgreSQL)
INSERT INTO users (id, email, name, role)
VALUES
  ('00000000-0000-0000-0000-000000000001', 'admin@example.com', 'Admin User', 'admin'),
  ('00000000-0000-0000-0000-000000000002', 'test@example.com', 'Test User', 'member')
ON CONFLICT (id) DO UPDATE SET
  email = EXCLUDED.email,
  name = EXCLUDED.name,
  role = EXCLUDED.role;

Documenting the Setup

The README should have a "Getting Started" section that is no more than 10 lines. If it is longer, your setup process is too complicated.

## Getting Started

Prerequisites: Docker, Node.js 20+

    git clone git@github.com:org/repo.git
    cd repo
    make setup
    make run

Open http://localhost:3000. Login with test@example.com / password.

That is the entire onboarding document for running the project. Everything else -- architecture, deployment, contributing guidelines -- goes in separate docs. The first thing a new developer sees should be "here is how to run it" in 30 seconds of reading.

Handling Platform Differences

macOS, Linux, and Windows developers on the same team will hit different issues. Docker behaves differently on each platform. File system case sensitivity varies. Shell scripts assume bash.

Minimize platform-specific instructions:

- Use Docker for services (works the same everywhere)
- Use cross-platform tools (Node, Python, Go) instead of shell scripts
- Avoid bash-specific syntax in Makefiles
- Test your setup on all platforms your team uses
- Document known platform issues when they cannot be avoided

If the team is split across platforms, Docker-based dev environments (covered in the dev containers subtopic) eliminate the problem entirely. But for many teams, Docker for services and native tooling for the application is the right balance of convenience and performance.

Common Pitfalls

  • Setup instructions that are out of date. The README says Node 18, the project requires Node 20. Test your setup instructions quarterly by following them from scratch.
  • Requiring real API keys for local development. If a developer needs a Stripe API key to start the application, mock it. Use local stubs for external services.
  • No seed data. Developers waste 30 minutes creating test data manually. Provide it in the setup script.
  • Global dependencies. "First install PostgreSQL 16, Redis 7, and Elasticsearch 8 on your machine" is not a setup process. It is a weekend project. Use Docker.
  • Hidden state. The application only works because you ran a migration 3 months ago that is not in the setup script. Wipe your local environment regularly to catch this.
  • Docker Compose without health checks. The application starts before the database is ready and crashes. Add health checks and use --wait.
  • The 200-line README. Nobody reads a 200-line getting-started guide. If your setup is that complicated, the problem is the setup, not the documentation.

Key Takeaways

  • "One command to set up, one command to run" is the standard. make setup && make run should get any developer from zero to a working application.
  • Use Docker Compose for services (databases, caches, queues). Pin versions. Add health checks.
  • Provide a .env.example with working defaults. No developer should need to ask what variables are required.
  • Include realistic seed data that is idempotent. An empty database is almost as useless as a broken setup.
  • Keep the Getting Started section under 10 lines. If it is longer, simplify the setup, not just the documentation.
  • Test the setup from scratch quarterly. Setup processes rot silently.