Docker Compose, Containerization, Multi-Container Applications, Container Networking, Service Dependencies, Application Configuration, Local Development Environments, Docker, Containers, Distributed Systems, Application Architecture, Service Architecture, Development Environments, DevOps, The Kingdom in the Clouds
The Kingdom in the Clouds

The Caravan of Services: Composing Containerized Applications

One wagon may travel alone, but a kingdom moves as a caravan.

Beyond the Castle Gate: From Containers to Applications

Packing a workshop into a container solves an important problem, but it does not solve the entire journey. In the previous chapter of The Kingdom in the Clouds, we explored how containers package an application together with the runtime, libraries, and dependencies it needs to operate consistently. That gives engineers a portable unit that can travel from a development machine to a test environment and eventually into production with far fewer surprises. Yet most modern applications are not single workshops sealed inside single wagons. They are collections of services, databases, caches, queues, and supporting systems that must travel together without becoming tangled together.

A web application might need an application server to process requests, a database to preserve information, and a cache to make frequently requested data available quickly. Another system might add a background worker, a message broker, an authentication service, or a reverse proxy. Each component can live inside its own container, giving every service a controlled environment and a clear responsibility. The difficult question then shifts from how to package a single application to how several independently packaged components become a dependable system. This is the problem of container composition, where containerization begins to look less like packing luggage and more like organizing a caravan.

Every wagon may be self-contained, but the system succeeds only when those wagons know where they belong, how to communicate, what supplies they carry, and what should happen when one falls behind. The engineering challenge is no longer merely isolation. It is coordination. That shift matters because the moment multiple containers depend on one another, the application gains relationships, shared expectations, and failure modes that did not exist when the software traveled alone.

The Gathering Caravan: One Application, Many Services

Consider a small web application built from three components. The first container runs the web application itself. The second runs PostgreSQL and stores the application’s persistent data. The third runs Redis and provides a fast cache for information that the application frequently requests. Each container has a different job, different dependencies, and potentially a different lifecycle.

Keeping those responsibilities separate provides real architectural value. The application container does not need PostgreSQL installed, and the database container does not need the application’s source code. Redis can be upgraded or replaced without rebuilding the entire system, while the application can be redeployed without treating the database as disposable cargo. Each service becomes an independently managed piece of infrastructure that participates in the larger application.

The arrangement might be imagined like this:

Web Application
      |
      +------ PostgreSQL
      |
      +------ Redis

The diagram looks almost trivial, but several engineering questions are already hiding inside those three lines. How does the web application locate PostgreSQL? Which network allows the containers to communicate? Where does the database store information that must survive a container replacement? How does the application receive database credentials without embedding them in its image? What happens if the web container starts before PostgreSQL is ready to accept connections?

A functioning multi-container application must answer every one of those questions. Composition provides a way to describe those relationships explicitly rather than relying on a collection of commands, undocumented assumptions, and tribal memory. That makes the environment easier to reproduce, but it also forces engineers to acknowledge the application’s structure.

The Caravan Manifest: Describing the System

Docker Compose is one commonly used tool for describing and running multi-container applications. Instead of launching each container separately and remembering every option required to connect them, engineers describe the desired services in a Compose file. The file becomes a declarative description of the local application environment and the relationships among its major components.

A simplified example might look like this:

</> YAML

services:
  web:
    build: .
    ports:
      - 8080:8080
    environment:
      DATABASE_HOST: db
      CACHE_HOST: cache
    depends_on:
      - db
      - cache

  db:
    image: postgres
    environment:
      POSTGRES_DB: kingdom
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: example
    volumes:
      - database-data:/var/lib/postgresql/data

  cache:
    image: redis

volumes:
  database-data:

The important lesson is not the YAML syntax. Syntax can be looked up, generated, forgotten, and looked up again. What matters is that the file expresses an architectural model. There is a web service, a database service, and a cache service. The application depends on supporting infrastructure; the database requires persistent storage, and configuration passes into containers through their environments.

This makes the application environment reproducible. A new developer does not need a handwritten page explaining which database version to install, which cache server to configure, and which ports to open. The composition describes those expectations in a form that tools can execute, creating a stronger engineering contract than a sentence buried halfway down a setup document. It also provides the team with a shared artifact that shows how the system is expected to run.

The Roads Between Wagons: Container Networking

Containers gain much of their usefulness from isolation, but complete isolation would make multi-container applications nearly useless. Services must communicate. A web server must reach its database, an API may need a message broker, and a worker might need both a queue and an object store. Composition therefore needs a networking model that preserves isolation while providing deliberate paths between services.

Docker Compose typically creates a network for the application and connects its services to it. Services can then find one another by service name. In the earlier example, the web container can refer to the PostgreSQL service as db and Redis as cache. It does not need to know which temporary IP addresses Docker assigns.

That distinction matters more than it initially appears. Infrastructure that depends on specific container IP addresses becomes brittle because containers are disposable. A container can disappear and return with a different address, while the role it performs remains the same. Service names provide a stable abstraction over that changing infrastructure, allowing the application to care about the identity of the service rather than the temporary location of one particular container.

This is another step away from thinking of infrastructure as a collection of machines. The application does not need to know that PostgreSQL occupies a particular numbered room in a particular castle. It needs a reliable way to find the database service. That same principle becomes much more important when the kingdom grows beyond one Docker host and begins operating across clusters of machines.

The Supply Wagons: Persistent Data in a Disposable World

Containers are deliberately replaceable. That makes application deployment easier, but it creates an obvious problem for databases. Losing a web container might mean losing a few seconds while another instance starts. Losing the database files every time its container is replaced would turn ordinary deployment into a particularly efficient form of catastrophe.

Persistent data, therefore, needs a lifecycle separate from the container that uses it. Docker volumes provide one mechanism for doing this. In the Compose example, the database-data volume is mounted into the PostgreSQL container. PostgreSQL can be removed, recreated, or upgraded while the underlying database files remain outside that individual container’s writable layer.

The service remains disposable while its important state survives. That separation illustrates one of the most important habits in container architecture: identify what is replaceable and what must endure. Application processes are often easy to recreate, while customer records, uploaded files, transaction histories, and other persistent state are not.

Good container composition makes that distinction explicit rather than discovering it after the wagon carrying the royal archives rolls into a ravine. It also establishes a pattern that will continue throughout this article: containers give us flexibility, but only when we are deliberate about the boundaries between temporary infrastructure and durable state.

Sealed Orders and Supply Lists: Configuration Without Rebuilding

A container image should describe what a service needs to run, but it should not permanently encode every detail about where or how that service will run. Database addresses, feature flags, API endpoints, logging levels, and environment-specific settings often change between development, testing, and production. If every configuration change requires rebuilding the image, the image ceases to be a portable artifact and becomes tied to a particular destination.

Environment variables provide one common way to keep those concerns separate. The application image contains the code and runtime, while the Compose configuration supplies values appropriate for the current environment. The same web image might connect to a local database during development and a managed database service in production without changing the application binary itself. The wagon remains the same even when the road signs change.

That distinction also helps teams separate configuration from source code. Developers can define sensible defaults in an application while allowing deployment-specific values to be injected later. A DATABASE_HOST variable can point to db in a local Compose environment and to another endpoint elsewhere, while a LOG_LEVEL variable can remain quiet during ordinary operation and become more detailed during troubleshooting. The container remains consistent while its environment supplies the context.

Secrets require greater care. Passwords, API keys, private certificates, and access tokens should not be baked into container images or casually committed to Compose files. Images are copied, cached, pushed to registries, and shared across systems, which makes embedded credentials difficult to contain once they escape. For local development, environment files may be convenient, but production systems generally require stronger secret management mechanisms and tighter access controls. The important architectural principle is simple: configuration may travel with the deployment, but secrets should never become permanent cargo inside the wagon itself.

The Order of the March: Dependencies Are Not Readiness

When several services start together, it is tempting to imagine that they rise in a neat sequence. The database starts, then the cache, then the web application, and everything proceeds across the drawbridge in orderly formation. Real systems are less cooperative.

A declaration such as depends_on can express that one Compose service depends on another, but dependency and readiness are not the same thing. A database container may have started its process while PostgreSQL is still initializing internal structures. A web application that immediately attempts a connection can fail even though the database container is technically running.

This distinction appears everywhere in distributed systems because started does not mean ready, and reachable does not mean healthy. Engineers must design services with that reality in mind. Health checks can verify whether a service can perform useful work, while application connection logic can use retries and backoff rather than assuming every dependency will be ready at precisely the right moment.

That may sound like defensive programming around an inconvenient startup problem, but it represents something larger. In a composed application, services operate on different timelines. They can start, restart, fail, recover, and become unavailable independently. Reliable systems treat those states as normal possibilities rather than exceptional events that should never occur.

The system does not collapse because one component pauses to recover. It collapses when every other component was designed under the assumption that recovery would never be necessary.

The Broken Axle: Designing for Service Failure

Containers encourage a useful kind of engineering pessimism. Processes fail. Networks drop connections. Databases restart. Caches disappear. Dependencies respond slowly. A service that behaves perfectly only when every other service behaves perfectly is not reliable. It is merely lucky.

Suppose the Redis cache in our example stops responding. If the cache exists only to improve performance, the web service might fall back to PostgreSQL until Redis returns. Requests could slow down, but the application would continue to function. That is a different failure profile from PostgreSQL becoming unavailable, which may prevent the application from fulfilling most requests because persistent data is essential.

Even then, the correct response is rarely to spin endlessly, consume every available connection, and produce mysterious timeouts. The application should fail predictably, report the problem clearly, and recover when the dependency becomes available again. These choices reveal that not all dependencies are equally important. Some are essential, while others are optional optimizations.

A simple diagram of services does not capture these differences:

Web
 | \
 |  \
DB  Cache

The lines look identical, but their meanings may be completely different. The database relationship can be critical, whereas the cache relationship is opportunistic. The architecture lives not only in which services connect, but also in what happens when those connections fail.

The Quartermaster’s Ledger: Logs and Observability

Once an application is divided into several services, troubleshooting changes. A single-process application may have one obvious log file and one obvious place to investigate. A composed application can produce logs from the web service, database, cache, reverse proxy, workers, and other supporting systems. The failure a user sees in one service may have originated several steps away.

Imagine that a user receives an HTTP 500 response. The web application logs might show that a database request timed out. PostgreSQL logs might reveal that it exhausted available connections. A worker service may be holding those connections because a downstream queue is responding slowly. The visible error occurs at the front gate, but the real problem sits several components deeper in the system.

This is why logs should be treated as part of application design rather than decoration added after deployment. Containers commonly write logs to standard output and standard error so the surrounding platform can collect and manage them. Engineers can then inspect individual service output or send logs to centralized systems that make events easier to search and correlate.

Useful logs provide context. A message stating request failed is almost ceremonial in its uselessness. A better event might include the service, operation, request identifier, involved dependency, duration, and error category without exposing sensitive information. When several services participate in a single request, shared correlation identifiers can enable tracking the request across service boundaries.

As systems grow, metrics and traces join logs to form a broader observability strategy. Metrics can reveal patterns such as rising response times or exhausted database connections. Distributed traces can show how a request moved through multiple services and where time was spent. We do not need a massive observability platform to run three containers on a laptop, but the architecture is already teaching us why those tools eventually become necessary.

More services create more independence, and more independence creates more places where reality can hide.

The Map Versus the Territory: Development Parity

One of the strongest arguments for Docker Compose is its ability to provide developers with a repeatable local environment. Instead of asking every engineer to manually install matching versions of PostgreSQL, Redis, and other dependencies, the project can describe them as containers. A new developer can clone the project, build the application, start the composition, and work with an environment that resembles the one used by the rest of the team.

That is valuable, but similarity should not be confused with identity. A development environment running three containers on one laptop is not equivalent to a production system distributed across several machines, availability zones, or managed cloud services. Networking characteristics differ, storage behaves differently, resource limits may differ, production secrets are managed differently, and real traffic creates concurrency patterns a local developer may never see.

The goal is therefore not perfect environmental duplication. It is meaningful parity. The interfaces and important behaviors should remain consistent enough that engineers can reason about the application locally without pretending that their laptop has somehow become a miniature production data center.

For example, a development Compose stack might run PostgreSQL in a local container while production uses a managed PostgreSQL service. Those environments differ operationally, but the application’s contract with the database remains largely the same. The developer still writes against PostgreSQL, still supplies a connection string through configuration, and still expects the same database semantics.

Container composition works best when it reproduces the application relationships that matter while allowing infrastructure details to vary where appropriate.

The Temptation of the Giant Wagon: One Container or Many?

Once engineers discover containers, another temptation appears: placing the entire application stack into a single container. The web server, database, cache, scheduler, and background worker can technically be installed together. One image could contain everything, and one container could launch everything. The whole kingdom could be lashed onto one enormous wagon and dragged down the road.

Technically possible is not the same as architecturally wise. Separating services into containers gives each component its own lifecycle. The web application can be rebuilt without changing the database; Redis can restart without restarting PostgreSQL; and a worker can be scaled independently of the frontend. Logs become easier to attribute, resource consumption becomes easier to understand, and failures remain more contained.

That does not mean every process deserves its own container merely because separation is available. Excessive fragmentation can create unnecessary complexity. An application sliced into dozens of tiny services without a meaningful architectural reason can become harder to understand, deploy, test, and troubleshoot than the system it replaced.

The useful principle is not one process per container at all costs. It is one coherent responsibility per container. A container boundary should explain the architecture by separating components that have different dependencies, scaling needs, security concerns, deployment lifecycles, ownership boundaries, or operational responsibilities. If no meaningful boundary exists, adding another container may simply create another component that someone must maintain.

The Shape of the Caravan: Composition as Architecture

A Compose file may initially appear to be infrastructure configuration, but it quickly becomes something more revealing. It shows the major runtime components of an application and their relationships. In that sense, it is also a compact architectural document.

Consider what a new engineer can learn from a well-written composition. They can see that the application requires PostgreSQL and Redis. They can identify which ports are exposed, which services communicate internally, where persistent storage is required, which environment variables configure the system, and which components depend on others.

That visibility matters because architecture often becomes difficult to understand when it exists only as scattered assumptions. One developer remembers that Redis must be running. Another knows about a background worker. Someone else knows which directory must persist between deployments. Eventually, the application depends on knowledge that exists nowhere but within the team.

A declarative composition pulls many of those assumptions into the open. It does not replace documentation, architectural diagrams, or engineering judgment. It does something more practical by requiring the application to specify enough of its operational structure for software to actually assemble it.

The metaphor has become architecture now. We have components with separate responsibilities, networks connecting them, persistent storage carried outside disposable containers, configuration supplied at deployment, readiness checks, and plans for failure. What began as a few container commands has started to resemble a small distributed system, and small distributed systems have a habit of wanting to grow.

The Roads Grow Crowded: When Composition Meets Scale

Docker Compose is remarkably effective when a system requires multiple cooperating containers on a single development machine or a relatively simple host. It lets engineers describe services, networks, volumes, configuration, and dependencies in one place. For local development and many small deployments, that may be exactly the right amount of machinery. Not every kingdom requires an imperial bureaucracy.

The limitations appear when the application begins to outgrow a single host. Imagine that the web service now needs ten instances instead of one. Traffic rises unpredictably throughout the day. Containers must be distributed across several servers, unhealthy instances must be replaced automatically, deployments must occur without taking the application offline, and workloads must continue running even when one machine fails.

At that point, the system needs someone to manage the whole arrangement. This distinction separates composition from orchestration. Composition explains which services belong together and how they interact. Orchestration manages those services across a larger pool of computing resources by deciding where containers should run, monitoring their health, replacing failed workloads, distributing traffic, and maintaining the application’s desired state.

That does not make Compose obsolete. It means the engineering problem has changed. A map showing several services and their dependencies may be sufficient when a single application runs on a single host. Once many instances spread across many machines, something must begin coordinating the movement.

The Merchant’s Mistake: Containers Are Not Microservices

Container composition also exposes a common architectural misunderstanding. Because containers make it easy to run services separately, teams sometimes assume that putting code into multiple containers automatically creates a microservices architecture.

It does not. A container is a deployment boundary, while a microservice is an architectural boundary. Those concepts can align, but they are not interchangeable.

A traditional monolithic application can run perfectly well inside one container. It can also use separate containers for supporting systems such as PostgreSQL, Redis, and a reverse proxy without becoming a microservices architecture. Conversely, a microservices system may use containers as its deployment mechanism because they fit the operational needs of independently deployed services.

The difference lies in the software design. Microservices typically divide a larger system into independently deployable business capabilities with explicit interfaces and some degree of autonomy. Containers merely provide isolated runtime environments in which software can execute. Confusing the two can lead teams toward unnecessary complexity, especially when a system is divided into many containers that still share one database schema, must always be deployed together, and cannot function independently.

The question should never be, “How many containers can we create?” The better question is, “Which boundaries make this system easier to change, operate, secure, and understand?” Containers can support good boundaries, but they cannot invent them for us.

The Cost of More Wagons: Complexity Has Weight

Separating application components provides flexibility, but every boundary creates operational cost. A function call within a single process is simple. A request traveling over a network between two services can fail, time out, arrive twice, arrive late, or succeed while the caller believes it failed. The moment software crosses a network boundary, engineers inherit problems that did not exist within a single process.

Authentication may be required between services. Network traffic may need encryption. APIs require versioning. Timeouts must be chosen. Retries must avoid creating request storms. Logs from multiple components must be correlated, and deployments must account for old and new service versions running simultaneously.

None of those challenges mean service separation is wrong. They mean separation should earn its place. Creating another service in a Compose file might require only a handful of lines, but the operational consequences of that service can last for years.

Good engineering therefore balances isolation against simplicity. Sometimes the correct architecture is a web container, a database, and a cache. Sometimes it is one application container and one database. Sometimes a larger system genuinely benefits from dozens of independently deployed services. The system should contain as many boundaries as the journey requires, not as many as the tooling makes easy to create.

The Watchtowers Between Camps: Security Across Services

Multiple containers also change the security model. A single application process already has an attack surface, but a composed system introduces communication paths between services, exposed ports, credentials, images from external registries, persistent volumes, and configuration flowing through the environment. Each of those becomes something engineers must consider deliberately.

One useful starting principle is to expose only what needs to be exposed. A database used exclusively by the web application usually does not need to have a port exposed to the outside world. The web service can communicate with it over the internal container network. The same applies to caches, queues, and internal APIs that do not serve external clients.

This is a small example of least privilege applied to architecture. Services should receive only the network access, credentials, filesystem permissions, and capabilities they need to perform their responsibilities. A compromised web container should not automatically become a master key to every other component in the system.

Images deserve the same scrutiny. Pulling an image from a registry is remarkably convenient, but an image is executable supply brought inside the walls. Engineers should understand where images come from, prefer trusted sources, pin versions appropriately, scan for known vulnerabilities when practical, and rebuild images as security updates become available.

Secrets require careful handling as well. Credentials should be scoped to the service that needs them, rather than distributed across every container just because it is convenient. A cache that never communicates with the database does not need database credentials, and a frontend should not possess administrative secrets simply because another service requires them. Container isolation helps create security boundaries, but those boundaries are only useful when engineers actually respect them.

The Road Tested Before Dawn: Testing the Composition

One advantage of a composed application is that engineers can test relationships between real services earlier in development. Unit tests remain valuable for testing small pieces of logic, but many failures occur at the boundaries between components. A query may work against an in-memory substitute but fail against the real database version. A serialization assumption may break when a worker reads messages from an actual queue. A connection timeout may behave differently than a mock suggested.

Containers make integration environments easier to reproduce. A test workflow can start the required services, initialize known data, run tests against them, and tear down the environment afterward. Continuous integration systems can use similar techniques to provide isolated environments for builds and tests, while the same service definitions developers use locally can reinforce a shared understanding of the application’s dependencies.

This does not eliminate the need for mocks, unit tests, staging environments, or other forms of testing. It gives engineers another layer between isolated code tests and full production deployment. That layer is especially useful because the most dangerous assumptions in composed systems often live between services.

Does the application really reconnect after PostgreSQL restarts? Does the worker handle a temporary Redis outage? Does the database migration complete before the new application version begins handling requests? Does the service fail clearly when required configuration is missing? Those questions belong to the system, not merely to individual containers.

A caravan should discover that two wagons cannot cross the same bridge during rehearsal, not while the king is riding in one of them.

The Royal Surveyor: Making Boundaries Visible

As containerized applications grow, diagrams become increasingly valuable. A simple architecture might begin like this:

                Internet
                   |
                   v
             +-----------+
             |    Web    |
             +-----------+
               /       \
              v         v
       +----------+  +-------+
       |PostgreSQL|  | Redis |
       +----------+  +-------+

Later, the application might add a worker and message queue:

                Internet
                   |
                   v
             +-----------+
             |    Web    |
             +-----------+
               /   |   \
              /    |    \
             v     v     v
       +--------+ +-----+ +-------+
       |Postgres| |Redis| | Queue |
       +--------+ +-----+ +-------+
                             |
                             v
                         +--------+
                         | Worker |
                         +--------+

The second diagram does more than add boxes. It changes the system’s failure modes, deployment relationships, security boundaries, and operational responsibilities. Every new service creates another boundary that must be understood, every arrow represents communication that can fail, and every persistent component carries state that must be protected.

This is why container composition should be approached as architecture rather than merely tooling. Diagrams make those relationships visible before complexity disappears beneath configuration files. The ability to see the system clearly is one of the quietest forms of engineering leverage.

The Camp Before the Mountain Pass: Knowing When Compose Is Enough

There is a tendency in software engineering to assume that the larger tool is automatically the more professional tool. If Docker Compose can run several containers and Kubernetes can orchestrate thousands of workloads, then Kubernetes must be the more serious choice for every project. That reasoning is how teams occasionally construct siege engines to open kitchen cupboards.

Compose can be entirely appropriate for development environments, demonstrations, small internal applications, test systems, and workloads that comfortably run on a single machine. Its relative simplicity can be an advantage, as engineers can understand the entire environment without first learning an entire orchestration platform.

The point at which another system becomes necessary depends on operational requirements rather than fashion. If the application must automatically recover workloads across multiple machines, scale services independently across a cluster, perform sophisticated rolling deployments, manage complex service discovery, or maintain a desired state across significant infrastructure, then orchestration begins to solve genuine problems.

Until those problems exist, additional machinery can create more burden than value. The best architecture is not the architecture containing the greatest number of technologies. It is the architecture that solves today’s problems while leaving a reasonable path toward tomorrow’s.

A caravan does not need an empire’s transportation ministry before the first wagon leaves the gate.

Beyond the Horizon: From Composition to Orchestration

We began this journey with a single container. It gave us a portable workshop whose dependencies could travel with the application. Then the workshop met the rest of the kingdom.

A database joined the road, followed by a cache and perhaps a queue, workers, proxies, and supporting services. Networking connected them. Volumes protected persistent state. Configuration allowed the same images to operate in different environments. Health checks and resilient connection logic acknowledged that dependencies would not always be ready, while logs and observability helped us understand what happened when the system began behaving strangely.

The important transformation was not merely technical. Our mental model changed. We stopped thinking of an application as a single executable running on a single machine and began thinking of it as a collection of cooperating services, each with its own responsibility, lifecycle, dependencies, state, and failure modes.

Docker Compose provides a practical way to describe that arrangement and reproduce it without manually assembling the entire environment each time. For a while, that can be enough. Then the kingdom grows.

One web container becomes ten. One server becomes several. A failed machine can no longer be an evening emergency requiring someone to connect remotely and restart processes. Deployments must continue while users remain online. Services must discover one another even as individual instances appear and disappear. The system needs to maintain a desired state despite constant movement underneath it.

At that point, the challenge is no longer how to compose the caravan. It is how to govern a thousand moving castles.

The Next Gate: The Keeper of a Thousand Castles

This week in The Kingdom in the Clouds, our journey through Leaving the Castle has moved from portable containers to applications assembled from many cooperating services. We have seen that container composition is not simply a convenient way to start several processes. It is an architectural exercise in defining boundaries, communication, persistence, configuration, resilience, security, and responsibility.

The lesson before we travel farther is simple. Independence creates flexibility, but flexibility creates coordination problems. Every service we separate gains its own freedom to start, stop, scale, fail, and recover, and eventually something must keep track of all that motion.

On Friday, we will climb another level higher. In The Keeper of a Thousand Castles: Understanding Kubernetes, we will explore what happens when containers must run across multiple machines rather than on a single host. We will examine pods, deployments, services, desired state, scheduling, scaling, and self-healing, not as a collection of mysterious Kubernetes vocabulary but as answers to problems our composition has already begun to encounter.

One wagon showed us portability. The caravan showed us composition.

Now the kingdom needs a keeper.

Leave a Reply

Your email address will not be published. Required fields are marked *