## Intro

A production-like local environment pays for itself quickly. When your app, database, cache, and proxy run together under Docker Compose with realistic settings, you reveal integration issues early, debug faster, and reduce surprises in staging and production. This guide shows how to build that setup with services, networks, volumes, environment files, health checks, and a practical debugging workflow.

## Workflow Overview

Use a simple loop that mirrors how software is delivered:

- Plan

- List services and how they talk (ports, protocols, environment variables).

- Decide networks, volumes, and health criteria for each service.

- Build

- Create production-like Dockerfiles (small images, non-root users, pinned versions).

- Use docker build or let Compose build from your Dockerfile.

- Run

- Start the stack with docker compose up -d .

- Wait for health checks to become healthy.

- Test

- Send traffic through the public entry point (for example, Nginx on port 80).

- Validate readiness, liveness, and core use cases.

- Iterate

- Change configs or code, rebuild, and rerun.

This clear separation of steps helps keep changes understandable and reduces rework.

## Compose Design

Below is a practical docker-compose.yml for a small web stack: a Node.js app, Postgres, Redis, and Nginx as the edge proxy. It uses two networks (frontend and backend), named volumes for data, env files for settings, and health checks with startup ordering.

version: '3.9'

services:
 web:
 build:
 context: ./web
 image: local/web:dev
 ports:
 - '8080:3000'
 env_file:
 - ./env/web.env
 environment:
 - NODE_ENV=development
 depends_on:
 db:
 condition: service_healthy
 redis:
 condition: service_healthy
 healthcheck:
 test: ['CMD-SHELL', 'wget -qO- http://localhost:3000/health || exit 1']
 interval: 10s
 timeout: 3s
 retries: 5
 start_period: 15s
 volumes:
 - ./web:/app:cached
 - web-node-modules:/app/node_modules
 networks:
 - frontend
 - backend

 db:
 image: postgres:16-alpine
 environment:
 POSTGRES_USER: app
 POSTGRES_PASSWORD: app
 POSTGRES_DB: appdb
 volumes:
 - db-data:/var/lib/postgresql/data
 healthcheck:
 test: ['CMD-SHELL', 'pg_isready -U app -d appdb']
 interval: 10s
 timeout: 5s
 retries: 5
 networks:
 - backend

 redis:
 image: redis:7-alpine
 command: ['redis-server', '--appendonly', 'yes']
 volumes:
 - redis-data:/data
 healthcheck:
 test: ['CMD', 'redis-cli', 'ping']
 interval: 10s
 timeout: 3s
 retries: 5
 networks:
 - backend

 nginx:
 image: nginx:1.25-alpine
 depends_on:
 web:
 condition: service_healthy
 ports:
 - '80:80'
 volumes:
 - ./nginx/conf.d:/etc/nginx/conf.d:ro
 networks:
 - frontend

volumes:
 db-data:
 redis-data:
 web-node-modules:

networks:
 frontend:
 backend: 

### Nginx config example

 Route traffic on port 80 to the web service.

# ./nginx/conf.d/app.conf
server {
 listen 80;
 server_name _;

 location / {
 proxy_pass http://web:3000;
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 }

 location /healthz {
 return 200 'ok';
 add_header Content-Type text/plain;
 }
} 

### Node.js Dockerfile example

 Keep it close to production: minimal base, non-root, pinned version, and a simple health endpoint in your app at /health .

# ./web/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
RUN addgroup -S app && adduser -S app -G app && chown -R app:app /app
USER app
EXPOSE 3000
CMD node server.js 

### Minimal Node app example

 // ./web/server.js
const express = require('express');
const app = express();
const port = 3000;

app.get('/health', (req, res) => res.send('ok'));
app.get('/', (req, res) => res.send('Hello from web via Nginx'));

app.listen(port, () => console.log(`web listening on ${port}`)); 

## Env and Secrets

 Use a project-level .env file for Compose variables, and per-service env files for app settings.

- Project .env controls Compose behavior and shared values.

- Service env files control application settings.

- Do not commit real secrets. Provide sample files and load real values from a secure location.

Example project .env :

# ./.env
COMPOSE_PROJECT_NAME=prodlike
WEB_PORT=8080 
 Example service env file:

# ./env/web.env
PORT=3000
DATABASE_URL=postgresql://app:app@db:5432/appdb
REDIS_URL=redis://redis:6379/0 
 You can reference ${WEB_PORT} in your compose file if you prefer to avoid hardcoding ports.

## Health Checks and Order

Health checks make dependency readiness explicit. Use healthcheck in each service and depends_on with condition: service_healthy in dependents. This prevents the web service from starting before the database is ready.

- Web health : GET /health returns 200.

- Postgres health : pg_isready passes.

- Redis health : redis-cli ping returns PONG.

Compose will show health in docker compose ps so you can see when the stack is ready.

## Bring it up, test it, and iterate

Start the stack:

docker compose up -d --build 
 Check status and health:

docker compose ps
docker compose logs -f --tail=100 web db redis nginx 
 Test through the proxy like a user would:

curl -sS http://localhost/ 
 Hot-reload options:

- Bind-mount ./web into the container for rapid local edits.

- If you need nodemon, add it to devDependencies and run it with an override compose file only for local use.

Rebuild when Dockerfile changes:

docker compose build web && docker compose up -d web 

## Debugging and Troubleshooting

 Common checks and fixes:

- Port conflicts

- Symptom: docker compose up fails to expose a port.

- Fix: change host ports (e.g., WEB_PORT ), or stop the conflicting process.

- Health check keeps failing

- Check container logs: docker compose logs web .

- Exec into the container and curl the health endpoint locally: docker compose exec web sh -lc 'wget -qO- http://localhost:3000/health' .

- Verify env variables are set: docker compose exec web env | sort .

- App cannot reach db or redis

- Verify DNS within the network: docker compose exec web getent hosts db redis .

- Test TCP reachability: docker compose exec web sh -lc 'nc -zv db 5432' and nc -zv redis 6379 .

- Volume permissions on Linux

- Symptom: EACCES on write.

- Fix: set the same UID/GID inside the container as your host user, or chown the workdir during build as shown in the Dockerfile.

- Slow file sync on macOS/Windows

- Symptom: slow hot reload.

- Fix: use the :cached option, move heavy dependencies to a named volume ( web-node-modules ), or build images more often instead of live-mounting everything.

- Data resets unexpectedly

- Ensure you use named volumes ( db-data , redis-data ) rather than anonymous volumes.

- Removing with docker compose down -v will delete named volumes; use with care.

- Environment not applied

- Check resolution order: .env , shell, and env_file .

- Print env: docker compose config to see the final merged config.

Useful commands:

docker compose config
docker compose ps --format json | jq '.'
docker compose logs -f web
docker compose exec -it web sh
docker compose inspect web

# Networks and volumes
docker network ls
docker network inspect prodlike_backend
docker volume ls 

## Local Pilot Plan

 Start small, measure, then expand.

### Scope

- Services: web + db only.

- Success criteria:

- Time from docker compose up -d to both services healthy is under 15 seconds on your machine.

- curl http://localhost/ returns 200 and expected content.

- docker compose restart web recovers to healthy within 5 seconds.

### Steps

- Implement health endpoints and checks for web and db.

- Add a named volume for Postgres data.

- Wire env via ./env/web.env and ./.env .

- Measure baseline timings with the commands in this guide.

- Add Redis and Nginx when the pilot meets the success criteria.

### Why this works

- Narrow scope is easier to debug and reason about.

- Clear, measurable outcomes tell you when to grow the stack.

## Conclusion

A production-like Docker Compose setup gives you fast, realistic feedback without leaving your laptop. Model your stack with services, networks, and volumes; wire configuration through env files; and use health checks and a repeatable workflow to reduce rework. Start with a narrow pilot, measure results, and expand toward full parity.