Docker Compose for 42 Projects

DevOps·7 min

At 42, infrastructure projects like ft_transcendence require orchestrating multiple services — web servers, databases, reverse proxies, monitoring stacks — all inside Docker containers. Getting this right from the start saves hours of debugging later.

The Problem

A typical 42 infrastructure project might include a Next.js frontend, a Spring Boot backend, PostgreSQL, Redis, Nginx as a reverse proxy, and Prometheus + Grafana for monitoring. That's 7+ services that need to communicate, persist data, and restart gracefully.

Without a clear architecture, you end up with spaghetti configs, port conflicts, and containers that can't find each other. Docker Compose solves this — but only if you structure it intentionally.

Project Structure

I organize my Docker projects with a clear separation between service configs, shared resources, and orchestration:

Project layout
project/
├── docker-compose.yml          # Main orchestration
├── docker-compose.override.yml # Dev overrides
├── .env                        # Environment variables
├── Makefile                    # Automation commands
├── srcs/
│   ├── frontend/
│   │   ├── Dockerfile
│   │   └── ...
│   ├── backend/
│   │   ├── Dockerfile
│   │   └── ...
│   ├── nginx/
│   │   ├── Dockerfile
│   │   └── conf/
│   └── monitoring/
│       ├── prometheus.yml
│       └── grafana/
└── volumes/                    # Persistent data

Compose Architecture

The key principle is: each service does one thing. The reverse proxy handles SSL and routing. The app servers handle business logic. The database handles persistence. No service should do another service's job.

docker-compose.yml
services:
  nginx:
    build: ./srcs/nginx
    ports:
      - "443:443"
    depends_on:
      frontend:
        condition: service_healthy
      backend:
        condition: service_healthy
    networks:
      - frontend-net
    restart: unless-stopped

  frontend:
    build: ./srcs/frontend
    expose:
      - "3000"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000"]
      interval: 30s
      timeout: 5s
      retries: 3
    networks:
      - frontend-net
    restart: unless-stopped

  backend:
    build: ./srcs/backend
    expose:
      - "8080"
    depends_on:
      db:
        condition: service_healthy
    env_file: .env
    networks:
      - frontend-net
      - backend-net
    restart: unless-stopped

  db:
    image: postgres:16-alpine
    volumes:
      - db-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USER"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - backend-net
    restart: unless-stopped

Service Networking

I use separate Docker networks to enforce isolation. The frontend network connects Nginx to the app servers. The backend network connects app servers to databases. The database is never directly accessible from Nginx.

Use 'expose' instead of 'ports' for internal services. 'expose' makes the port available within the Docker network without mapping it to the host — reducing your attack surface.

Volume Management

Data persistence is critical. Without named volumes, your database is wiped every time you rebuild. I use named volumes for all stateful services and bind mounts only for development hot-reloading.

  • Named volumes for databases (db-data) — survive rebuilds
  • Bind mounts for source code in dev — enable hot reload
  • tmpfs for ephemeral data (sessions, caches) — fast and auto-cleaned
  • Never use anonymous volumes — they're hard to track and clean up

Set proper ownership inside your Dockerfiles. PostgreSQL containers run as uid 999 by default. If your volume directory has root ownership, the container will fail with permission errors.

Health Checks

The depends_on directive alone only checks if a container is running — not if the service inside is ready. Health checks solve this by verifying the application is actually accepting connections before dependent services start.

srcs/backend/Dockerfile
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/*.jar app.jar

HEALTHCHECK --interval=15s --timeout=5s --retries=3 \
  CMD wget -qO- http://localhost:8080/actuator/health || exit 1

ENTRYPOINT ["java", "-jar", "app.jar"]

Makefile Integration

A Makefile wraps Docker Compose commands with validation, color-coded output, and common workflows. This is the interface your teammates actually use.

Makefile
NAME    := ft_transcendence
COMPOSE := docker compose

.PHONY: all up down re logs clean fclean

all: up

up:
	@echo "\033[32m[✓] Starting $(NAME)...\033[0m"
	$(COMPOSE) up -d --build
	@echo "\033[32m[✓] All services running.\033[0m"

down:
	$(COMPOSE) down

re: down up

logs:
	$(COMPOSE) logs -f --tail=50

clean: down
	$(COMPOSE) down --rmi local -v

fclean: clean
	docker system prune -af --volumes

The key lesson from 42 infrastructure projects: invest time in your Docker architecture early. A well-structured Compose file pays dividends in debugging time, onboarding speed, and deployment reliability.