Thought
Technology
Before the first commit: the four development infrastructure decisions that prevent most production bugs

Setting up the development infrastructure before writing code prevents most "production bugs" that turn out to be environment failures. Four decisions matter most: OS choice (compatibility), IDE (productivity and error reduction), version control (team collisions and lost work), and package management (dependency hell and environment drift). Modern teams add containers, CI/CD, and secrets management on top.
The bug that wasn't a bug
A pattern most developers recognize: a "production bug" gets reported, the team scrambles to investigate, and the cause turns out to have nothing to do with the code. The function the developer wrote behaves correctly. What broke was the environment. A different version of a dependency in production than in staging. An environment variable that exists locally but wasn't set in deployment. A subtle difference in how the production OS handles a system call. Configuration drift that nobody noticed until it caused a visible failure.
These aren't code bugs in any meaningful sense. They're setup failures, and they're expensive because the team usually spends hours assuming the code is wrong before checking the environment.
The pattern is preventable. Most of these failures trace back to development infrastructure decisions made early in a project (or skipped entirely because someone wanted to start coding faster). The team that invests in setup before they write the first line of code spends less time debugging environment mysteries later. The team that skips setup pays the cost in production incidents over the lifetime of the project.
This article covers the four foundational decisions we make before any project's first commit, the modern practices that layer on top, and the common setup mistakes that cause the most pain in real-world development.
For the production-side counterpart of this, our article on backend infrastructure covers the equivalent decisions for the live environment that the development infrastructure here is supporting.
Four foundational decisions
Each decision answers a different risk category. Get them wrong and the cost shows up later as bugs that don't look like bugs.
1. Operating System (OS)
What it is: The system the development team runs on locally and the system the code will run on in production. Common choices: Linux distributions (Ubuntu, Debian, Alpine), macOS for development, Windows for some enterprise environments.
What it prevents: Compatibility surprises. Code that works on macOS may behave differently on Linux due to filesystem case sensitivity, different system libraries, or differences in how the OS handles specific edge cases. Production servers usually run Linux, so if developers are working on macOS or Windows, the team needs a way to validate that the code works in a Linux environment before deployment.
Common practice: Many teams develop on macOS or Windows but run their code in Linux containers (Docker) so the code executes against the same OS as production from day one. This matches the OS layer between development and production without forcing every developer to switch operating systems.
2. IDE (Integrated Development Environment)
What it is: The editor and toolchain the developer uses to write, test, and debug code. Common options include VS Code (especially for JavaScript, TypeScript, and a wide range of languages), PyCharm or JetBrains IDEs (strong for Python and JVM languages), Vim or Neovim for terminal-first developers, and Xcode for iOS or macOS development.
What it prevents: Slow iteration and avoidable errors. A good IDE catches type mistakes before runtime, suggests autocompletes, integrates the debugger, runs tests, and integrates with version control. The difference between a developer with a well-configured IDE and one without is significant in daily output.
Common practice: Standardize the team on one or two IDEs (or on the same set of plugins/extensions across IDEs) so configuration files like linting rules, formatter settings, and debugger configurations are shared. Personal preference matters, but inconsistency creates friction during code reviews and pairing.
3. Version Control
What it is: The system that tracks changes to the codebase over time. Effectively this means Git, plus a hosting platform (GitHub, GitLab, Bitbucket) for collaboration.
What it prevents: Lost work, team collisions, and the inability to roll back when something breaks. Without version control, two developers editing the same file overwrite each other. With version control, they can work in parallel through branches, merge changes deliberately, and roll back specific changes if something breaks. The history of who changed what and when becomes a debugging tool in itself.
Common practice: Set up the repository on day one before writing any code. Establish a branching strategy (trunk-based development, GitHub Flow, GitFlow, or a variant) and document it. Configure branch protection rules so production branches can't be pushed to directly. Add a basic README and contributing guide so new team members can ramp up quickly.
4. Package Management
What it is: The system for installing, updating, and locking the external libraries the project depends on. Examples: npm or yarn for JavaScript, pip or Poetry for Python, Bundler for Ruby, Cargo for Rust, Go modules for Go.
What it prevents: Dependency hell and environment drift. Without locked dependencies, developer A installs version 2.4 of a library, developer B installs version 2.5 a week later, and they get different behavior on the same code. Production might end up with version 2.6, and now you have three slightly different environments running the same code.
Common practice: Use a lockfile (package-lock.json, yarn.lock, requirements.txt with pinned versions, Pipfile.lock, etc.) and commit it to version control. The lockfile guarantees that everyone on the team and every deployment uses identical dependency versions. Update dependencies deliberately, with testing, rather than letting them drift.
Production-staging parity (the principle that prevents most environment bugs)
Beyond the four foundational decisions, one principle governs how to set up the rest of the development pipeline: production and staging environments should match production as closely as possible.
The principle is simple. The closer staging is to production, the more confidence you can place in tests that pass in staging. The further apart they drift, the more you're testing something that doesn't reflect what will actually run in front of users.
What "match" means in practice:
- Same OS and OS version. If production runs Ubuntu 24.04 LTS, staging runs Ubuntu 24.04 LTS.
- Same database engine and version. If production runs PostgreSQL 16, staging runs PostgreSQL 16, not 14 or 15.
- Same library versions. Lockfiles handle this for application code. Make sure the same applies to system-level dependencies.
- Matching environment variables. Different values are fine (and usually necessary). Different sets of variables that exist isn't fine. If production has FEATURE_FLAG_X set, staging should too, even if to a different value.
- Same configuration approach. If production reads configuration from environment variables, staging does too. If production uses a secrets manager, staging uses one too (even if it's a different instance).
What goes wrong without parity: code that passes all tests in staging fails in production for reasons no one anticipated. The team spends hours investigating before someone notices that the production OS has a slightly different system library, or that production sets an environment variable that staging doesn't. These are the bugs that look like code problems but are actually setup problems.
Maintaining parity isn't free. It requires discipline (don't update one environment without updating the other) and tooling (containers and infrastructure-as-code make parity much easier to maintain). But the alternative is paying the cost in incidents.
Modern infrastructure on top of the four
The four decisions above are foundational. Modern development practice typically adds three more layers on top of them.
Containerization (Docker): Docker packages an application together with its dependencies and a specific OS environment, producing a container that runs the same way everywhere. The team's local development, CI pipeline, staging, and production all run the same container image. This solves "works on my machine" problems and makes production-staging parity a property of the infrastructure rather than a discipline the team has to maintain manually.
CI/CD (Continuous Integration / Continuous Deployment): Automated pipelines that run tests on every commit, build the application, and deploy it to staging or production based on rules. The pipeline runs in a clean environment every time, catching environment-specific issues that might not appear on a developer's local machine. CI/CD also means deployments are routine rather than risky events; if every push runs through the same automated pipeline, deploying becomes a known process rather than a special-case operation.
Secrets management: A system for handling sensitive configuration (API keys, database passwords, OAuth tokens) without putting them in source code. Common approaches: environment variables loaded from a .env file (with the file itself excluded from version control), or a dedicated secrets manager (AWS Secrets Manager, HashiCorp Vault, 1Password Secrets Automation). Hardcoding secrets in source code is one of the most common security mistakes, and it's almost always preventable with basic secrets management discipline.
These three layers aren't optional in 2025 dev practice. Teams that skip them are paying ongoing costs in deployment friction, security risk, and debugging time.
Common setup mistakes
Patterns to watch for. Each one has a specific cost.
- Hardcoded credentials in source code. Database passwords, API keys, OAuth secrets pasted directly into a config file. The first developer to commit this exposes the secret in the repository's history forever, even if it gets removed later. Costly to fix, even more costly when an attacker finds the exposed credentials in a public repo. Use environment variables and a secrets manager from day one.
- Inconsistent dependency versions across team members. Developer A installs the latest version of a library; Developer B installs a slightly different version a week later. Bugs that appear on one machine and not another waste hours. Lockfiles solve this. Commit the lockfile.
- Local development that doesn't match production. Developer's macOS handles a system call one way, production Linux handles it differently, and the bug only appears in production. Containerize the development environment or use a Linux dev container so the OS layer matches.
- Missing CI/CD. "We deploy manually when needed" is a setup that works at scale 1 and breaks at scale 5. Manual deployments produce inconsistent results, are easy to skip steps in, and don't catch the regressions an automated test suite would catch. Set up basic CI on day one, even for small projects.
- Manual secrets handling. Each developer has their own copy of .env with their own values. Secrets get out of sync, new developers spend a day figuring out which values they need, and onboarding becomes friction. A shared secrets management approach (or at minimum, a clear documentation of what variables are needed and where to get them) avoids this.
- No documented setup process. A new team member spends their first week figuring out how to run the project locally. The first developer who set it up holds the knowledge in their head. Write down how to set up the project and keep the docs updated.
These mistakes are individually solvable. Together, they're the difference between a team that ships smoothly and one that fights its own infrastructure constantly.
The takeaway
Development infrastructure isn't glamorous work. Setting up Git, configuring linters, writing a Dockerfile, putting together a CI pipeline, organizing secrets management. None of it is the work that ships features. But the time invested in this foundation pays back over the lifetime of the project in fewer environment bugs, faster onboarding, more confident deployments, and less time spent debugging things that aren't actually code problems.
The four decisions covered above (OS, IDE, version control, package management) are the foundation. The three modern layers (containers, CI/CD, secrets management) build on it. The principle that ties them together is parity: keeping development, staging, and production close enough to each other that what works in one works in the others.
Teams that get this right don't think about it day to day, because it works. Teams that get it wrong spend significant time chasing the consequences, often without realizing the original cause is in the setup decisions made (or skipped) at the start.
FAQ
Why does development environment setup matter so much?
Why is Git or version control essential for team development?
Why should staging and production environments match?
What's the difference between an IDE and a code editor?
Writer
Front-End Developer
Lanyana Chansawang