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 downandcompose upcycles
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:
+------------------------+ +------------------------+ +------------------------+| 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:
+------------------------------------------+| 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_healthyDetailed Step-by-Step Practical Lab
Milestone 1 — Write the base docker-compose.yml
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
mkdir -p db-- 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
## 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## Confirm hot reload is workingdocker compose up -decho "console.log('hot reload test')" >> src/index.jsdocker compose logs -f api## nodemon restarts the process automatically on file changeMilestone 4 — Move secrets and config into .env
## .env file -- never commit this to gitcat > .env << 'EOF'POSTGRES_USER=swiggyPOSTGRES_PASSWORD=swiggy_dev_pwPOSTGRES_DB=swiggy_ordersNODE_ENV=developmentEOF ## .env.example -- safe to commit, shows required keys with no real valuescat > .env.example << 'EOF'POSTGRES_USER=POSTGRES_PASSWORD=POSTGRES_DB=NODE_ENV=EOF echo ".env" >> .gitignore## Reference .env variables inside docker-compose.ymlservices: postgres: environment: POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB}Milestone 5 — Separate dev-only settings with an override file
## docker-compose.override.yml is loaded automatically alongside## docker-compose.yml -- no extra flag neededcat > 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 + overridedocker compose config## Shows the fully merged, resolved configurationMilestone 6 — Persist data across down and up cycles
## Stop the stack -- named volumes are preserved by defaultdocker compose down ## Start again -- orders table data from earlier is still theredocker compose up -ddocker compose exec postgres psql -U swiggy -d swiggy_orders -c "SELECT * FROM orders;" ## Only use --volumes when you intentionally want a clean slatedocker compose down --volumesProduction 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 |
PLACEMENT PRO TIP**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 THIS**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 / WARNING**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.
COMMON MISTAKE / WARNING**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.