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/
├── 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 dataCompose 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.
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-stoppedService 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.
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY target/*.jar app.jar
HEALTHCHECK \
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.
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 --volumesThe 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.