API
DB
Redis
compose

Docker Compose for multi-project local dev

Use Docker Compose to run multiple projects side by side, avoid port conflicts, isolate MySQL and Redis, and keep local dev easy to reset.

14 min read15/06/2026

The real problem

One dev machine often needs to run multiple projects at the same time:

  • project A needs MySQL + Redis
  • project B also needs MySQL, but different data
  • project C adds MongoDB

If you install everything directly on the host, you quickly get:

  • port conflicts
  • version drift
  • mixed local data across projects

The clean fix is simple: each project gets its own docker-compose.yml, with its own app, MySQL, and Redis.

A complete compose file first

If the real goal is "give me one compose file I can use right now so each project has isolated MySQL and Redis without port conflicts", start with this:

# docker-compose.yml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile.dev
    ports:
      - "3000:3000"
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      NODE_ENV: development
      DB_HOST: mysql
      DB_PORT: 3306
      DB_USER: root
      DB_PASS: localroot
      DB_NAME: mydb
      REDIS_URL: redis://redis:6379
    depends_on:
      mysql:
        condition: service_healthy
      redis:
        condition: service_started

  mysql:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: localroot
      MYSQL_DATABASE: mydb
    ports:
      - "3307:3306"
    volumes:
      - mysql_data:/var/lib/mysql
    healthcheck:
      test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-plocalroot"]
      interval: 5s
      timeout: 5s
      retries: 10
      start_period: 20s

  redis:
    image: redis:7-alpine
    ports:
      - "6380:6379"
    volumes:
      - redis_data:/data

volumes:
  mysql_data:
  redis_data:

Dockerfile.dev:

FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD ["npm", "run", "start:dev"]

Start it:

docker compose up -d
docker compose logs -f api

With this file:

  • the app runs at localhost:3000
  • this project's MySQL runs at localhost:3307
  • this project's Redis runs at localhost:6380
  • MySQL and Redis data live in isolated volumes

If another project also uses Compose, you only change the host ports:

  • project A: 3307, 6380
  • project B: 3308, 6381
  • project C: 3309, 6382

That is the core trick for avoiding port conflicts while keeping each project fully isolated.

Break down the file piece by piece

Why does api use DB_HOST=mysql instead of localhost?

Because inside a Docker Compose network, services talk to each other by service name.

In this file:

  • api reaches MySQL through mysql:3306
  • api reaches Redis through redis:6379

localhost is only for connections coming from the host machine.

Why is MySQL 3307:3306 and Redis 6380:6379?

The format is:

HOST_PORT:CONTAINER_PORT

That means:

  • the host uses 3307
  • inside the MySQL container it is still 3306

This lets every project keep the same internal config while only changing external host ports.

Example for another project:

mysql:
  ports:
    - "3308:3306"

redis:
  ports:
    - "6381:6379"

Why mount the whole project with - .:/app?

For local dev, this is often better than mounting only ./src:/app/src because:

  • file changes are visible immediately
  • many frameworks need config files outside src
  • you avoid missing folders like prisma, migrations, uploads, or framework config files

This line:

- /app/node_modules

prevents the host directory from overwriting the container's node_modules.

Why do MySQL and Redis need their own volumes?

Without volumes, deleting the containers also deletes the data.

This part keeps data persistent:

volumes:
  - mysql_data:/var/lib/mysql
  - redis_data:/data

That is exactly what makes multi-project local dev cleaner:

  • each project gets its own data
  • resetting project A does not wipe project B

What do depends_on and healthcheck solve?

Many apps fail on startup because the API comes up before MySQL is actually ready.

In this file:

  • mysql has a healthcheck
  • api waits for MySQL to become healthy first

That reduces common startup issues like:

  • ECONNREFUSED
  • database not ready
  • migrations running too early

The commands make more sense after the file

Once the compose file is clear, the commands are straightforward:

# start the full stack
docker compose up -d

# tail app logs
docker compose logs -f api

# list running containers
docker compose ps

# open a shell inside the app container
docker compose exec api sh

# stop the stack
docker compose down

# stop everything and remove data volumes too
docker compose down -v

docker compose down -v is especially useful when you want to reset only that project's database without cleaning up your whole machine manually.

When should you expose MySQL or Redis to the host?

In local dev, usually only when you need:

  • TablePlus or DBeaver
  • a Redis GUI or direct host-side debugging

If you do not need that, you can remove the ports section entirely for MySQL and Redis. The app will still connect internally by service name.

A stricter version:

mysql:
  image: mysql:8
  volumes:
    - mysql_data:/var/lib/mysql

redis:
  image: redis:7-alpine

That means:

  • the app still uses mysql:3306 and redis:6379
  • but the host machine cannot access those services directly anymore

When should each project have its own MySQL and Redis?

You usually want separate services when:

  • each project has its own schema
  • you want down -v on one project without affecting another
  • different projects need different MySQL or Redis versions

Trying to force all projects to share one local MySQL sounds clean at first, but it often turns into mixed data and config drift.

Compose File for Production

Production needs a few differences:

  • No source code bind mounts (build a complete image)
  • Don't expose database ports to the host
  • Add restart policies
  • Use environment variable injection, not hardcoded secrets
# docker-compose.prod.yml
services:
  api:
    image: myapp:latest      # pre-built image, no rebuild on server
    ports:
      - "3000:3000"
    environment:
      NODE_ENV: production
      DB_HOST: mysql
      DB_PASS: ${DB_PASSWORD}
      REDIS_URL: redis://redis:6379
    restart: unless-stopped
    depends_on:
      - mysql
      - redis

  mysql:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
      MYSQL_DATABASE: mydb
    volumes:
      - mysql_data:/var/lib/mysql
    # No port exposed to host in production
    restart: unless-stopped

  redis:
    image: redis:7-alpine
    volumes:
      - redis_data:/data
    restart: unless-stopped

  nginx:
    image: nginx:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx.conf:/etc/nginx/nginx.conf:ro
      - ./certs:/etc/nginx/certs:ro
    depends_on:
      - api
    restart: unless-stopped

volumes:
  mysql_data:
  redis_data:

Deploy:

# Build new image
docker build -t myapp:latest .

# Pull latest image (if using a registry)
docker compose -f docker-compose.prod.yml pull

# Restart just the api service with new image, no downtime for others
docker compose -f docker-compose.prod.yml up -d --no-deps api

Most Useful Commands

# Start all services in detach mode
docker compose up -d

# Stop all services
docker compose down

# Stop and remove volumes (deletes data)
docker compose down -v

# Tail logs for a service
docker compose logs -f api

# Restart a service
docker compose restart api

# Open a shell inside a container
docker compose exec api sh

# Check service status
docker compose ps

# Rebuild image (after Dockerfile changes)
docker compose build api

# Run multiple instances of a service
docker compose up -d --scale api=3

Healthcheck — Wait Until Services Are Ready

APIs frequently crash on startup if the database isn't ready yet. Use healthcheck + depends_on condition to handle it:

mysql:
  image: mysql:8
  healthcheck:
    test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p${MYSQL_ROOT_PASSWORD}"]
    interval: 5s
    timeout: 5s
    retries: 10
    start_period: 30s

api:
  depends_on:
    mysql:
      condition: service_healthy   # waits until mysql is healthy before starting

Summary

Docker Compose isn't just a convenience tool for spinning up databases — it's the standard way to manage multi-service applications from dev to production:

Install directly Docker Compose
New machine setup Reinstall everything docker compose up
Multiple projects Port conflicts, version clashes Each project isolated
Team consistency "Works on my machine" Same config for everyone
Cleanup Leftover files everywhere docker compose down -v
Production Separate config Same file, different env vars

Once you get used to Compose, it's very hard to go back to installing services directly on the machine.