Kahibaro
Discord Login Register

13.5 Common Development Patterns

Overview

In day to day development you will not just run a single container manually. Instead, you will repeat certain ways of structuring services, source code, configuration, and Docker commands. These recurring ways of working are development patterns. In this chapter you will learn the most common development patterns that use Docker, how they feel in practice, and when each one fits best. You will not learn how to configure CI systems or how to deploy to production here, because those topics have their own dedicated chapters.

Single Container per Developer Pattern

The simplest pattern is a single container that a developer runs locally. The source code usually lives on the host machine, inside your normal project folder. Docker is used to provide a consistent runtime, not the code itself.

In this pattern you build or pull one image that contains the runtime you need. For example Python with some native libraries, Node.js with a specific version, or Java with a particular JDK. You then start a container that mounts the current project directory as a volume. The container becomes your development shell or application runner, while the files stay editable through your normal editor on your host.

The main benefit of this pattern is that all developers share the same runtime environment without fighting over tool versions on their own machines. Debugging is straightforward because there is only one process to think about, and there is only one Docker container involved. This pattern is ideal for simple apps, command line tools, or libraries that talk only to external services.

Multi Container Local Stack Pattern

Modern applications rarely live alone. They might need a database, cache, message broker, and maybe a separate frontend. The multi container local stack pattern uses Docker to assemble all of these services on your laptop so you can work on one part of the system while the others run nearby.

In this pattern each service in your stack runs in its own container. The app you are actively developing might use your source code from a bind mount and might be rebuilt frequently. The supporting services such as databases and queues are usually pulled from public images and configured via environment variables and volumes for persistent data. All containers run together, often coordinated by Docker Compose, which is explained in detail in other chapters.

The key idea of this pattern is that your laptop simulates a small version of a production environment. You can run end to end flows, test integration behavior, and share the same setup with your team. The cost is that the environment becomes more complex. You must manage configuration, ports, and data directories for several containers at once. For anything beyond trivial applications this pattern becomes the default way to work with Docker in development.

Disposable Environment Pattern

Another frequent pattern is to treat the entire development environment as disposable. Instead of maintaining long lived containers and volumes, you create fresh environments on demand and delete them once you are done. With Docker this is very natural because starting containers is fast and images can be cached locally.

In this pattern you might have a script or a small set of commands that spin up your complete stack from scratch. They pull images, create networks, create volumes, run migrations or seed data, and finally start application containers. When you no longer need this environment you stop and remove all containers and optionally delete the volumes. The next time you run the script you receive a clean environment again.

This pattern helps you avoid hidden state that causes confusing bugs. It encourages repeatability because the same commands that initialize your local environment can also be used in test or staging environments. It is particularly useful if your application relies heavily on database schemas or message queues that tend to become polluted over time.

A disposable environment should be reproducible from version controlled files and commands. If you cannot recreate it from scratch, it is not truly disposable.

Shared Compose File Pattern

In many teams the primary way to describe a development environment is a docker-compose.yml file that everyone checks into the repository. This is the shared Compose file pattern. Instead of writing per developer instructions, the repository includes a canonical way to run the application stack.

In this pattern the Compose file defines all containers, their networks, volumes, and environment variables that are needed for local development. The same file is used by all developers. Over time the team may split it into multiple Compose files for different purposes, such as docker-compose.dev.yml and docker-compose.test.yml, but the principle stays the same. Every change to the environment is made in code and reviewed just like application code.

This pattern improves onboarding significantly. A new developer can often run one or two commands and have a working environment. It also helps documentation stay correct, since the Compose file and the actual environment are the same thing. The trade off is that you must be careful to keep development specific configuration separate from production configuration, because the Compose file tends to grow and accumulate options.

Containerized Tooling Pattern

Another pattern uses Docker not primarily for your application but for its supporting tools. Instead of installing compilers, linters, test runners, or package managers on each developer machine, you package these tools into Docker images and run them through containers.

In this pattern you might have images for running unit tests, static analysis, database migrations, or code generators. Makefiles, shell scripts, or package scripts then delegate to docker commands behind the scenes. A typical flow is that a developer runs a single command in the project directory, and under the hood a container is started with the right working directory and configuration.

The major benefit is that tool versions are tightly controlled. Everyone runs the same linter, the same build tool, and the same testing framework version, regardless of their host operating system. It also mirrors what happens in CI pipelines, since CI jobs can use the same images and run the same commands. A drawback is that each tool invocation now goes through Docker, which adds a little overhead and requires good scripting so the commands still feel natural.

Git Branch per Environment Pattern

Some teams adopt a pattern where each git branch corresponds to a particular application state and the Docker environment matches that branch exactly. This is the git branch per environment pattern. Whenever you switch branches, you rebuild or restart the containers to reflect the code and configuration on that branch.

In practice this can be implemented with naming conventions or small helper scripts. A script might build an image tagged with the branch name, start containers whose names include the branch name, and create volumes that are also specific to that branch. This way you can switch between features or bugfix branches without mixing data and containers.

This pattern is valuable when you need to juggle several long running tasks. You might have one branch where you are experimenting with a schema change and another where you are fixing a separate bug. Each branch keeps its own local environment with compatible data. The cost of this approach is higher disk usage and more complex cleanup, since each branch has its own images and volumes.

Configuration via Environment Variables Pattern

A universal pattern in Docker based development uses environment variables as the main configuration method. You do not hardcode database URLs, API keys, or feature flags into images. Instead, your containers receive configuration at runtime. In development, these values usually come from local .env files, Docker Compose environment sections, or direct command line options.

This pattern keeps images generic. The same image can run in development, testing, and production. Only the environment variables change. For development workflows this is important because you will rebuild images frequently and you do not want secrets or local paths burned into them. It also lets you easily start the same container with different settings to simulate multiple environments.

Never bake secrets directly into Docker images. Use environment variables or secret management systems so images stay reusable and safe to share.

Local Mock and Stub Pattern

During development you often need to talk to external services such as payment providers, identity providers, or third party APIs. The local mock and stub pattern uses containers to run mocked versions of these external systems on your machine.

In this pattern you run additional containers that simulate external behavior. They may be open source mock servers, small custom services, or simple HTTP endpoints that return fixed responses. Your application under development talks to these containers instead of the real external services. Configuration switches between real and mock endpoints using environment variables or configuration files.

The advantage is that your local development workflow becomes faster, safer, and more predictable. You can test failure scenarios and edge cases that would be hard or expensive against real services. The trade off is that mocks must stay reasonably in sync with real systems, otherwise your tests give a false sense of security.

Prebuilt Image for Developers Pattern

Some organizations distribute a prebuilt image that includes all tools needed for development along with standard editors, shells, and debugging aids. Developers might start this image in a container and then connect to it either through a terminal or through remote development features in modern IDEs.

In this pattern the development environment itself is containerized. The image might include language runtimes, build tools, database clients, and some default configuration. Source code can be mounted or cloned into the container. Some setups even run a full graphical desktop environment inside the container and expose it over a remote display protocol, though text based workflows are more common.

The main benefit is that onboarding becomes nearly instant. A new team member only needs Docker and a way to start the predefined development container. Everyone uses the same tools with the same versions. However, this approach requires careful maintenance of the development image and clear rules about how local customizations are handled. It is also heavier than other patterns, since the image can be large.

Pattern Selection and Combination

In real projects you rarely use just one pattern. You might have a shared Compose file that describes a multi container local stack. On top of that you might apply the disposable environment pattern for testing large changes, and containerized tooling for running tests and linting. Another team might combine prebuilt developer images with the single container per developer pattern and environment variable based configuration.

The key is to choose patterns that match your application complexity, team size, and tooling preferences. Simple services often do well with a single container pattern and some containerized tooling. Larger systems benefit from a multi container stack with a shared Compose file, disposable environments, and clear configuration management.

Over time your patterns will evolve. As your needs change, you can refactor scripts, Compose files, and images to better support the way your team works. Docker helps by making environment definitions explicit and repeatable, which is the foundation of every good development pattern.

Views: 5

Comments

Please login to add a comment.

Don't have an account? Register now!