The four stages worth having
1. Every push runs checks
Type checks, linting, and tests on every push to every branch. Not just the main branch — the value is catching problems before review, not after merge.
The hard requirement is speed. A suite that takes more than about ten minutes stops being a feedback loop and becomes something people work around. If yours is slower, splitting fast unit tests from slow integration tests and running the fast set first buys back most of the value.
2. Main is always deployable
Merging to main produces a tested, deployable artifact. Whether it deploys automatically is a separate choice; the artifact existing is the point.
This is what makes rollback possible, and it is why building on every merge matters even if you deploy weekly.
3. One command to deploy, one to roll back
The essential property is not automation — it is that deployment is identical every time and reversible. A scripted deploy triggered by a button beats a sophisticated pipeline nobody trusts.
Test the rollback path before you need it. An untested rollback is not a rollback; it is a hypothesis.
4. Migrations are separate from deploys
The most common way small teams break production is a schema change and a code change shipping as one atomic step that cannot be undone. Run migrations as an explicit step, make them backward compatible with the currently running code, and you keep the ability to roll back the application without restoring a database.
What you can postpone
Deliberately not required at this size:
- Multiple pre-production environments. One staging environment that resembles production is enough. Three environments mean three things to keep in sync.
- Blue-green or canary deploys. Valuable at traffic levels where a bad minute matters. Below that, a fast rollback is equivalent and far simpler.
- Infrastructure as code for everything. Worth it for what you rebuild or scale. Clicking together a one-off resource and documenting it is a defensible trade early on.
- A container orchestrator. Unless you already know you need it. A managed application platform removes an entire category of work.
- Automated dependency upgrades merging themselves. Automated pull requests are useful; automatic merging without review is how a small team gets a surprise.
Each of these solves a real problem you will probably have later. Adopting them before the problem exists means maintaining a solution and its complexity for free.
Branching that suits two people
Elaborate branching models exist to coordinate many people across parallel releases. With two engineers they add ceremony and little else.
Short-lived branches off main, merged within a day or two, is almost always right. The benefits are concrete: fewer conflicts, smaller reviews, and no long-lived branch quietly diverging.
Where a feature genuinely needs longer, put it behind a flag and merge it incomplete but inactive. A merged, dormant feature is far safer than a three-week branch, and it keeps the "main is deployable" property intact.
Secrets, briefly
The pipeline needs credentials, which is where small teams most often take a shortcut that becomes a problem.
The minimum bar, all of which is achievable on day one:
- No secrets in the repository, including in CI configuration files.
- Secrets injected at deploy time from a managed secret store or the CI provider's encrypted variables.
- Different credentials per environment, so a staging leak is not a production incident.
- A written note of what would need rotating and how, so it is a checklist rather than a research project under pressure.
Monitoring is part of the pipeline
Deployment automation without any signal about the result is half a system. The minimum that makes deploying feel safe:
- A health endpoint that checks the database connection and any critical dependency, and a deploy step that verifies it before finishing.
- Error tracking with alerting on a spike in the error rate, which catches the deploys that succeed technically and break functionally.
- Deploy markers in whatever monitoring you have, so "when did this start" has an obvious answer.
With those three, the loop is closed: you ship, you find out quickly if it went wrong, and you can undo it in one command. That is the whole goal, and everything beyond it is refinement.