Defining Services
[!NOTE] This module explores the core principles of Defining Services, deriving solutions from first principles and hardware constraints to build world-class, production-ready expertise.
1. What is a Service?
Imagine running a restaurant where the chefs, waiters, and dishwashers are all fighting over a single kitchen counter. Someone drops a pan, and the entire restaurant grinds to a halt. In the server world, this is a monolithic application running natively on a single OS.
In Docker Compose, a Service is the solution: it provides isolated, dedicated workspaces for each component. Technically, it is a declarative configuration for a container that tells Docker:
- Which image to use (or how to build it).
- What ports to open to the outside world.
- What volumes to mount for persistent storage.
- How to communicate with other services over a private network.
Ideally, one service equals one single process (e.g., web, worker, db).
2. Core Configuration
1. Build vs Image
You can either pull an image or build one from source.
services:
# Option A: Pull from Registry
redis:
image: redis:alpine
# Option B: Build from Dockerfile
api:
build:
context: ./api
dockerfile: Dockerfile.dev
args:
GO_VERSION: 1.21
2. Startup Order (depends_on)
By default, Compose starts services in parallel. This causes race conditions (e.g., the API tries to connect to the DB before the DB is ready).
depends_on defines dependency order.
Interactive: Dependency Graph
Toggle the dependencies to see how startup order changes.
3. Healthchecks (The “Real” dependency)
depends_on only waits for the container to start, not to be ready.
War Story: The Cascading Crash Loop
A team once deployed a Node.js API dependent on a massive PostgreSQL instance. They used
depends_on, but Postgres took 15 seconds to allocate its shared buffers before accepting connections. The Node.js API booted in 2 seconds, immediately tried to query the DB, failed, and crashed. The restart policy kicked in, crashing it again before Postgres ever finished booting.
To prevent this, you must explicitly instruct Docker to wait until the dependency is functionally operational.
Solution: Healthchecks.
services:
db:
image: postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
api:
depends_on:
db:
condition: service_healthy # Wait for GREEN status
3. Production Patterns
Restart Policies
What happens when your container crashes?
no: Do not restart (default).always: Always restart.on-failure: Restart only if exit code ≠ 0.unless-stopped: Always restart unless explicitly stopped by user.
services:
worker:
image: my-worker
restart: on-failure
Resource Limits (Deploy)
Prevent a memory leak from crashing your entire server.
services:
app:
deploy:
resources:
limits:
cpus: '0.50'
memory: 512M
4. Code Example: Robust Go Service
Here is how you structure a Go service in Compose with all best practices.
# docker-compose.yaml
services:
# The Application
app:
build: .
ports:
- "8080:8080"
environment:
- DB_HOST=db
- DB_PORT=5432
depends_on:
db:
condition: service_healthy
restart: always
# The Database
db:
image: postgres:15-alpine
environment:
- POSTGRES_PASSWORD=secret
volumes:
- pgdata:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
pgdata:
This configuration ensures:
- Zero Downtime: The app waits for the DB to be ready.
- Resilience: Both services restart automatically on failure.
- Persistence: Data is saved in a named volume.