Skip to main content

Docker Compose for Local Development — Full Stack in One Command

Build a complete local development stack with Docker Compose — API, PostgreSQL, Redis, and RabbitMQ — with hot reloading and clean config.

44 Terms

Overview and What You Will Learn

Every new engineer who joins the backend team at Swiggy faces the same first-day problem: the API needs PostgreSQL, Redis, and RabbitMQ running locally before a single test can pass. Installing all three natively on a laptop, keeping versions in sync with production, and avoiding port conflicts with other projects is a slow, error-prone setup.

Docker Compose collapses this entire setup into one command. This lab builds a real local development stack from scratch, the same shape used by backend teams running Node.js APIs against Postgres, Redis, and a message queue.

By the end of this lab you will:

  • Build a multi-service compose stack: API, PostgreSQL, Redis, RabbitMQ
  • Configure bind mounts so code changes reload instantly without rebuilding
  • Seed a database automatically on first startup
  • Keep secrets and config out of the compose file using .env
  • Separate development-only settings from the base compose file using an override file
  • Persist database data across compose down and compose up cycles

Why This Matters in Production

A development environment that does not match production causes the classic "works on my machine" failure. When the local Postgres version, Redis version, and network topology mirror what runs in mumbai-prod-cluster, bugs show up locally before they ever reach a pull request review.

At Swiggy, the onboarding doc for a new backend repo is often just three lines: clone the repo, copy .env.example to .env, run docker compose up. Within two minutes a new engineer has the full stack — API, database, cache, and queue — running locally with realistic data. That two-minute onboarding is the direct payoff of building the compose stack correctly once.

Core Principles

The full local development stack:

◈ DIAGRAM
+------------------------+ +------------------------+ +------------------------+
| api service | | postgres service | | redis service |
| | | | | |
| node:20-alpine | --> | postgres:16-alpine | | redis:7-alpine |
| bind mount: ./src | | named volume: pgdata | | named volume: redisdata|
| port 4000:4000 | | port 5432:5432 | | port 6379:6379 |
+------------------------+ +------------------------+ +------------------------+
|
v
+------------------------+
| rabbitmq service |
| |
| rabbitmq:3-management |
| named volume: rabbitdata |
| ports 5672, 15672 |
+------------------------+

How depends_on with health checks controls startup order:

◈ DIAGRAM
+------------------------------------------+
| postgres container starts |
+------------------------------------------+
|
v
+------------------------------------------+
| healthcheck polls pg_isready every 5s | <- container reports
+------------------------------------------+ 'starting' until ready
|
v
+------------------------------------------+
| status becomes healthy |
+------------------------------------------+
|
v
+------------------------------------------+
| api container is allowed to start | <- depends_on: condition
+------------------------------------------+ service_healthy

Detailed Step-by-Step Practical Lab

Milestone 1 — Write the base docker-compose.yml

YAML
services:
api:
build: .
container_name: swiggy-api-dev
ports:
- "4000:4000"
environment:
NODE_ENV: development
DATABASE_URL: postgres://swiggy:swiggy_dev_pw@postgres:5432/swiggy_orders
REDIS_URL: redis://redis:6379
RABBITMQ_URL: amqp://guest:guest@rabbitmq:5672
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_started
rabbitmq:
condition: service_healthy
postgres:
image: postgres:16-alpine
container_name: swiggy-postgres-dev
environment:
POSTGRES_USER: swiggy
POSTGRES_PASSWORD: swiggy_dev_pw
POSTGRES_DB: swiggy_orders
ports:
- "5432:5432"
volumes:
- pgdata:/var/lib/postgresql/data
# init script runs once on first container creation
- ./db/init.sql:/docker-entrypoint-initdb.d/init.sql
healthcheck:
test: ["CMD-SHELL", "pg_isready -U swiggy"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
container_name: swiggy-redis-dev
ports:
- "6379:6379"
volumes:
- redisdata:/data
rabbitmq:
image: rabbitmq:3-management-alpine
container_name: swiggy-rabbitmq-dev
ports:
- "5672:5672"
- "15672:15672"
volumes:
- rabbitdata:/var/lib/rabbitmq
healthcheck:
test: ["CMD", "rabbitmq-diagnostics", "check_port_connectivity"]
interval: 10s
timeout: 5s
retries: 5
volumes:
pgdata:
redisdata:
rabbitdata:

Milestone 2 — Add a database seed script

Bash
mkdir -p db
SQL
-- db/init.sql
-- Runs automatically on first container creation only
-- (docker-entrypoint-initdb.d scripts never run again once pgdata has content)
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
restaurant_id INTEGER NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'placed',
created_at TIMESTAMP DEFAULT now()
);
INSERT INTO orders (restaurant_id, status) VALUES
(101, 'delivered'),
(102, 'placed'),
(103, 'cancelled');

Milestone 3 — Configure bind mounts for hot reload

YAML
## In docker-compose.override.yml (see Milestone 5)
services:
api:
volumes:
- ./src:/app/src
- ./package.json:/app/package.json
# anonymous volume prevents host node_modules from
# overwriting the container's installed dependencies
- /app/node_modules
command: npm run dev
Bash
## Confirm hot reload is working
docker compose up -d
echo "console.log('hot reload test')" >> src/index.js
docker compose logs -f api
## nodemon restarts the process automatically on file change

Milestone 4 — Move secrets and config into .env

Bash
## .env file -- never commit this to git
cat > .env << 'EOF'
POSTGRES_USER=swiggy
POSTGRES_PASSWORD=swiggy_dev_pw
POSTGRES_DB=swiggy_orders
NODE_ENV=development
EOF
## .env.example -- safe to commit, shows required keys with no real values
cat > .env.example << 'EOF'
POSTGRES_USER=
POSTGRES_PASSWORD=
POSTGRES_DB=
NODE_ENV=
EOF
echo ".env" >> .gitignore
YAML
## Reference .env variables inside docker-compose.yml
services:
postgres:
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}

Milestone 5 — Separate dev-only settings with an override file

Bash
## docker-compose.override.yml is loaded automatically alongside
## docker-compose.yml -- no extra flag needed
cat > docker-compose.override.yml << 'EOF'
services:
api:
volumes:
- ./src:/app/src
- /app/node_modules
command: npm run dev
environment:
DEBUG: "swiggy:*"
EOF
## Compose automatically merges base + override
docker compose config
## Shows the fully merged, resolved configuration

Milestone 6 — Persist data across down and up cycles

Bash
## Stop the stack -- named volumes are preserved by default
docker compose down
## Start again -- orders table data from earlier is still there
docker compose up -d
docker compose exec postgres psql -U swiggy -d swiggy_orders -c "SELECT * FROM orders;"
## Only use --volumes when you intentionally want a clean slate
docker compose down --volumes

Production Best Practices and Common Pitfalls

Scenario Wrong Correct
Local node_modules syncing Bind mount entire app directory including node_modules Bind mount source only, anonymous-volume node_modules
Secrets in compose file Hardcode POSTGRES_PASSWORD directly in yaml Reference via ${VAR} from .env, .env in .gitignore
API starts before DB ready depends_on without healthcheck depends_on with condition: service_healthy
Re-running seed scripts Expecting init.sql to re-run after data exists init.sql only runs once on a fresh volume
Dev vs prod config mixing One docker-compose.yml with dev-only volumes Base file + docker-compose.override.yml for dev-only settings

Quick Reference and Troubleshooting Commands

Task Command
Start full stack docker compose up -d
Start and rebuild docker compose up -d --build
View merged config docker compose config
Tail logs for one service docker compose logs -f api
Run a one-off command docker compose exec api npm run migrate
Stop, keep volumes docker compose down
Stop, delete volumes docker compose down --volumes
Check service health docker compose ps
Tip

Run docker compose config whenever the stack misbehaves. It prints the final merged YAML after combining the base file and the override file, instantly revealing config mistakes that are invisible when reading the two files separately.

Remember

docker-compose.override.yml is loaded automatically by Compose with zero extra flags, as long as it sits next to docker-compose.yml. This is why it is the standard place for dev-only bind mounts and debug flags.

Common Mistake

Bind mounting the whole project directory and accidentally overwriting the container's node_modules with an empty host folder. Always add an anonymous volume entry for node_modules after the source bind mount to protect the container's own installed packages.

Security

.env files often contain real-looking development passwords. Even though they are "just dev," never commit them to git. Commit only .env.example with empty values, and add .env to .gitignore on day one of the repo.

Resources

Explore More in Docker Compose and Multi-Container Applications

All 6 Topics

Frequently Asked Questions

Is Docker Compose for Local Development — Full Stack in One Command free to learn on DevOps Network?

Yes - this topic, like everything on DevOps Network, is 100% free with no paywall or sign-up gate.

What does the Docker Compose for Local Development — Full Stack in One Command topic cover?

Build a complete local development stack with Docker Compose — API, PostgreSQL, Redis, and RabbitMQ — with hot reloading and clean config.