A Complete, End-to-End Guide for Modern Microservices
π Why Docker Compose?
When you have several microservicesβsay a Spring Boot API, a Node.js frontend, and a PostgreSQL databaseβmanually building and starting containers gets messy.
Docker Compose solves this by letting you:
-
Define all services in one file (
docker-compose.yml
) - Build images automatically from local Dockerfiles
- Create a shared network so containers talk to each other by name
- Scale & orchestrate them with a single command
ποΈ Project Structure
A scalable layout for two Java services and a database:
multi-service-app/
ββ service-a/
β ββ src/...
β ββ Dockerfile
β ββ pom.xml
ββ service-b/
β ββ src/...
β ββ Dockerfile
β ββ pom.xml
ββ database/
β ββ init.sql
ββ .env
ββ docker-compose.yml
Each service is independent, with its own Dockerfile and build artifacts.
π³ Step 1: Write Service Dockerfiles
Example service-a/Dockerfile
(Spring Boot JAR):
FROM eclipse-temurin:17-jdk-alpine
WORKDIR /app
COPY target/service-a.jar app.jar
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
Repeat similarly for service-b.
Tips
- Use a
.dockerignore
file to skiptarget/
ornode_modules/
for faster builds. - Pin your base images to a specific tag for reproducibility.
βοΈ Step 2: Create a .env
File
Centralize environment variables:
POSTGRES_USER=myuser
POSTGRES_PASSWORD=secret
POSTGRES_DB=mydb
SPRING_PROFILES_ACTIVE=prod
Docker Compose will automatically load this.
π Step 3: The docker-compose.yml
Hereβs a production-grade Compose file that:
- Builds both microservices from source
- Spins up a PostgreSQL database
- Assigns explicit container names
- Creates a dedicated bridge network
- Configures health checks and restart policies
version: "3.9"
networks:
app-net:
driver: bridge
volumes:
db-data:
services:
service-a:
container_name: service-a-container
build:
context: ./service-a
dockerfile: Dockerfile
image: myorg/service-a:latest
ports:
- "8081:8080"
networks:
- app-net
environment:
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
DB_URL: jdbc:postgresql://db:5432/${POSTGRES_DB}
DB_USER: ${POSTGRES_USER}
DB_PASS: ${POSTGRES_PASSWORD}
depends_on:
db:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
service-b:
container_name: service-b-container
build:
context: ./service-b
dockerfile: Dockerfile
image: myorg/service-b:latest
ports:
- "8082:8080"
networks:
- app-net
environment:
SPRING_PROFILES_ACTIVE: ${SPRING_PROFILES_ACTIVE}
SERVICE_A_URL: http://service-a:8080
depends_on:
service-a:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
db:
container_name: postgres-container
image: postgres:15-alpine
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
ports:
- "5432:5432"
volumes:
- db-data:/var/lib/postgresql/data
- ./database/init.sql:/docker-entrypoint-initdb.d/init.sql
networks:
- app-net
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
π Highlights
-
Networks:
app-net
gives each service a hostname equal to its service key (e.g.,db
,service-a
). -
Volumes:
db-data
persists database data across container restarts. - depends_on with healthcheck ensures dependent services wait until the database or other services are healthy.
-
Environment: pulled from
.env
for easy secrets management.
βΆοΈ Step 4: Build and Launch
From the project root:
docker compose up --build -d
This will:
-
Build
myorg/service-a
andmyorg/service-b
images. -
Start three containers (
service-a-container
,service-b-container
,postgres-container
). -
Attach them to the
app-net
network.
Check everything is running:
docker ps
docker compose logs -f
π Service-to-Service Communication
Inside app-net
:
-
service-b
can callservice-a
at http://service-a:8080 - Both services connect to Postgres at jdbc:postgresql://db:5432/mydb
Dockerβs internal DNS means no hardcoded IP addresses.
π οΈ Useful Lifecycle Commands
Action | Command |
---|---|
Stop containers | docker compose stop |
Restart with new code | docker compose up --build -d |
Tear down everything | docker compose down |
Remove images too | docker compose down --rmi all |
View container logs | docker compose logs -f service-a |
Scale a service (e.g. service-b) | docker compose up -d --scale service-b=3 |
π§° Best Practices & Pro Tips
Multi-Stage Builds:
Compile in a builder image, copy only the final artifact into a slim runtime image for smaller images.Secrets Management:
For production, use Docker secrets or a vault rather than plain.env
.Monitoring & Metrics:
Integrate Prometheus/Grafana by adding services to the same network.CI/CD Integration:
Usedocker compose build
in pipelines to create versioned images and push them to a registry.Resource Limits:
Adddeploy.resources.limits
to control CPU and memory usage in Swarm or Compose v3+.
β Recap
With this setup you can:
- Define, build, and run multiple microservices and a database with a single command.
- Give each container a clean name, dedicated network, and health checks.
- Scale horizontally and manage environment configs seamlessly.
This is a one-stop, production-grade solution to bootstrap your microservice architecture with Docker Compose.
Just add more services, copy the pattern, and your stack will grow cleanly and reliably.
Top comments (0)