14.3.2.1 Compose App Stack
A focused guide to Compose App Stack, connecting core concepts with practical Docker and container operations.
A Compose app stack is the complete set of interrelated services, networks, and volumes defined together in one or more Compose files, describing not just how to run each individual container but how the services as a whole are meant to fit together as a single, cohesive application.
The stack as the unit of design
Designing a Compose app stack starts from the application's actual architecture, not from individually convenient container configurations: which services need to talk to each other, which need to be reachable from outside the stack, and which data needs to be shared or kept isolated between services.
services:
frontend:
build: ./frontend
ports:
- "80:80"
api:
build: ./api
expose:
- "8080"
worker:
build: ./worker
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
cache:
image: redis:7
volumes:
pgdata:
This stack reflects a deliberate shape: a frontend and worker that each depend on the api and shared infrastructure, a database that only the api and worker need, and a cache shared between services that benefit from it, expressed entirely through which services reference which others and which ports are published versus only exposed internally.
Network segmentation within the stack
A Compose app stack does not need to put every service on one flat network; segmenting services into multiple networks based on what genuinely needs to reach what is a meaningful architectural decision expressed directly in the Compose file:
services:
frontend:
networks:
- public
api:
networks:
- public
- internal
db:
networks:
- internal
networks:
public:
internal:
With this layout, the database is reachable only by services attached to the internal network, and the frontend has no network path to the database at all, which enforces at the infrastructure level an isolation boundary that might otherwise only exist as an informal convention.
Service dependencies and startup ordering
The stack's dependency graph, which services must be ready before others start, should be made explicit rather than left to chance or handled entirely through application-level retry logic:
services:
api:
depends_on:
db:
condition: service_healthy
cache:
condition: service_started
db:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
retries: 10
Expressing this dependency graph directly in the stack definition means anyone reading the Compose file can understand the application's actual startup requirements without needing to read through application code or deployment scripts to discover them.
Shared versus service-owned volumes
A stack's volumes should be scoped to reflect actual data ownership: a volume genuinely shared between services because they both need direct filesystem access to the same data is architecturally different from a volume that happens to be defined in the same file but is only ever used by one service:
services:
api:
volumes:
- uploads:/app/uploads
worker:
volumes:
- uploads:/app/uploads
db:
volumes:
- pgdata:/var/lib/postgresql/data
volumes:
uploads:
pgdata:
Here, uploads is legitimately shared between api and worker, since both need to read and write the same files, while pgdata is exclusively owned by db, and naming and organizing volumes to reflect this distinction makes the stack's actual data architecture visible just from reading the file.
Scaling individual services within the stack
A Compose app stack does not require every service to scale uniformly; specifying replica counts per service reflects the reality that different components of the same application have different load characteristics:
services:
api:
deploy:
replicas: 3
worker:
deploy:
replicas: 6
db:
deploy:
replicas: 1
A stateless API service and a queue-processing worker often need very different replica counts to keep up with their respective workloads, while a single-instance database remains a deliberate constraint rather than an oversight, since most relational databases are not designed to run as multiple independent writers without additional clustering technology.
Environment-specific stack composition
The same base stack definition can be extended differently for different environments, adding or removing services as appropriate rather than maintaining entirely separate stack definitions per environment:
services:
mailhog:
image: mailhog/mailhog
ports:
- "8025:8025"
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d
docker compose -f docker-compose.yml -f docker-compose.production.yml up -d
A development-only override might add a mail-catching service that has no place in the production stack at all, while the production override might instead add resource limits and restart policies that development does not need.
Treating the stack definition as the architecture documentation
Because the Compose file fully describes the stack's services, their networks, their dependencies, and their data, it functions as living architecture documentation that stays accurate as long as it remains the actual deployment mechanism, rather than a diagram that can silently drift out of sync with what is really running:
docker compose config
Running config to print the fully resolved stack definition, after all override files have been merged, is a useful way to confirm the architecture as actually deployed matches the architecture as intended, especially once a stack has accumulated several layered override files over time.
Common mistakes
- Putting every service on a single flat network by default, missing an opportunity to enforce real isolation boundaries that the stack's own architecture calls for.
- Defining a shared volume between services that do not actually need to share data, blurring the stack's real data ownership boundaries.
- Relying entirely on application-level retry logic to handle startup ordering instead of expressing genuine dependencies and health conditions directly in the stack definition.
- Scaling every service in the stack identically regardless of their actual, differing load characteristics.
- Letting the Compose file drift out of sync with the application's actual architecture by patching production directly instead of updating the stack definition and redeploying from it.
A Compose app stack is most valuable when its file structure is treated as a direct, accurate expression of the application's real architecture, services, dependencies, network boundaries, and data ownership, rather than as an incidental list of containers that happen to be needed to make the application run.