Why Containerize Your Local Development Environment?
Every application running in production was originally developed on a software engineer’s laptop. That means every engineer has a local development environment — used for the IDE and for running tests locally.
Potential Problems
Monolithic Applications
If the project is a large monolith, there are typically both frontend and backend engineers. Taking backend engineers as an example, a backend engineer’s machine would have the following installed:
- Language runtimes, such as JDK, Node.js, etc.
- Databases, such as MySQL, PostgreSQL.
- Various other odds and ends.
If the project has only one backend engineer, the following problems may arise:
- The production environment differs from the local environment, leading to the classic “it works on my machine” situation where everything is fine locally but all sorts of strange issues appear in production.
- When switching to a new computer, the runtime environment must be manually reinstalled from scratch.
- OS upgrades may break the existing runtime environment.
If the project has multiple backend engineers, in addition to the problems a single engineer might face, the following issues can also emerge:
- Different engineers may be running different OS versions — usually not a problem, but when it is, it can be really painful.
- Engineers may have inconsistent runtime environments, whether it is a patch version difference or a minor version difference.
- Repetitive setup work gets done over and over by different people.
- Onboarding new team members is costly and requires detailed, step-by-step guidance.
- Even with thorough environment setup documentation, manual configuration errors still occur. Never trust manual operations — they are inherently unreliable, regardless of how skilled or unskilled the person performing them is.
If the project is later handed off to a separate operations team for maintenance, the following problems may arise:
- “How do I even start this locally?” Even when documentation exists, a lot of implicit context built up during development goes undocumented.
- How do you run the local tests?
- The ops team engineers need to install countless runtime environments on their own machines.
Microservices Applications
If a microservices application is developed by a single team, the following problems may arise:
- Runtime environments may be inconsistent between engineers.
- Different services use different runtimes, requiring multiple different environment versions to be configured locally.
If a microservices application is developed by multiple teams, in addition to the problems above, the following issues can also surface:
- Different technology stacks — for example, Team A uses Golang, Team B uses Java, Team C uses Node.js.
- Inconsistent code styles.
If the application is later handed off to a dedicated operations team, the following problems arise:
- The diversity of technology stacks keeps the ops team’s learning curve perpetually high.
- The same diversity makes it harder to keep local environments consistent with production.
- Inconsistent code styles mean that when fixing a bug, ops engineers may constantly switch between technology stacks and coding conventions — or even between different versions of the same stack — leading to an increasingly frustrating experience for the ops team.
There are likely many more problems I have not considered here.
Summary
To briefly summarize the issues described above:
- Inconsistent runtime environments: differences between engineers’ local setups, and differences between local development and production.
- Implicit context introduced by technology stack diversity.
- Inconsistent code styles.
Solutions
Based on the problems above, here are some approaches to address a few of them.
Inconsistent Runtime Environments
Among all the problems, inconsistent runtime environments are the most common and also the easiest to solve. For this category of problems, the simplest solution is containerization.
Two layers of containerization are needed:
- Containerize the local development environment.
- Containerize the production runtime environment.
I believe the vast majority of teams can containerize their production runtime environment, since it is not particularly difficult. What is most often overlooked is containerizing the local development environment.
Containerizing the local development environment goes a long way toward ensuring consistency between engineers and between the development environment and production. It also effectively resolves the various strange issues that arise from different services using different versions of their respective runtimes.
Technology Stack Diversity
Technology stack diversity is something we advocate and pursue — it is not inherently a problem. However, it inevitably introduces certain side effects. Implicit context is the most prominent of these, yet it is the least likely to receive attention.
What is implicit context? Here is a simple example:
- Team A develops in
node.js, manages packages withnpm, and may or may not put unit test commands inpackage.json. - Team B also develops in
node.js, but usesyarnfor package management. Likewise, unit test commands may or may not be inpackage.json. - Team C develops in
Java, usinggradlefor package management. - Team D develops in
Kotlin, usingmavenfor package management.
Team A knows how to start their own service locally and how to run unit tests. They may forget to document this in the README or package.json. One day, a member of Team B needs to change something in Team A’s codebase. Because the technology stacks are similar, Team B’s engineer — even after spending some time reading the code and making exploratory attempts based on their existing knowledge — can reasonably figure out how to start Team A’s service locally and how to run the tests.
But what if Team C needs to change something in Team A’s codebase?
And what if, one day, the entire microservices application is handed over to a standalone operations team?
This kind of knowledge — things that everyone within a team implicitly understands but that have never been surfaced through scripts or documentation — is what we call implicit context.
How do you address implicit context? Building on a containerized local development environment, automation scripts are a solid practice. For example, if every service repository includes an auto/test script responsible for running unit tests, does that not make it much easier to manage this implicit context across teams? Similarly, code style checks could be covered by a script named auto/check-style.
Inconsistent Code Styles
Inconsistent code styles across services are normal — different technology stacks naturally lead to different conventions.
So what is actually the problem?
- Inconsistent code style within a single service.
- Inconsistent code styles across services, with no strong tooling to enforce them.
How do you solve this? Every code style convention we follow should be managed and configured through tooling.
Summary
The solution to all of the problems described above comes down to two things: containerizing the local development environment and using automation scripts.