Small teams often skip CI/CD because it sounds like infrastructure for big companies. In practice, the smaller the team, the more it helps. When one person builds, deploys and supports a product, every manual deployment step is a chance to forget something at the worst moment, and every hour spent deploying by hand is an hour not spent on the product.
All my products, including NeonTask and Pro Clean+, run with a CI/CD pipeline that builds and ships every change. Here is what I automate, in the order I would recommend.
The goal: boring deployments
A good pipeline makes deploying a non-event. You merge a change, it is tested, built and released, and if something goes wrong you roll back in a minute. That changes how you work: you ship small changes often, which are easier to test and easier to undo, instead of big risky releases every few weeks.
Step 1: Everything in version control
Before any automation:
- All application code in Git.
- Infrastructure definitions too: Dockerfiles, compose files, reverse proxy config, deployment scripts.
- No secrets in the repository. Use the CI system's secret store or environment files on the server.
- A main branch that is always deployable.
Step 2: Checks on every push
The first automated stage runs on every push and pull request:
- Install dependencies with a lockfile, so builds are reproducible.
- Lint and type check. TypeScript and the .NET compiler catch a whole class of mistakes for free.
- Run unit tests on the critical business logic: pricing, availability, permissions, status transitions.
- Build the application.
Keep this fast, ideally a few minutes. A slow pipeline gets skipped. Cache dependencies between runs.
You do not need full test coverage to start. Tests on the logic that would hurt most if broken give most of the value.
Step 3: Build an artifact once
Build a Docker image for each service, tag it with the commit hash, and push it to a container registry. The image that passed tests is exactly the image that runs in production. Never rebuild on the server from source, because then what you tested is not what you ship.
Use multi-stage Dockerfiles so production images contain only what they need, which keeps them small and reduces the attack surface.
Step 4: Automated deployment
For a single server or a few servers running Docker Compose, deployment can be simple:
- The pipeline connects to the server over SSH with a dedicated deploy key.
- It updates the image tag in the environment or compose file.
- It pulls the new images and restarts the changed services.
- It runs a health check and fails the deployment if the service does not come up healthy.
With a reverse proxy in front and health checks, restarts are brief. For true zero-downtime, run two instances and switch traffic once the new one is healthy. I describe the single-server setup in running Docker Compose in production.
For projects where each client company runs its own deployment, as with NeonTask, the pipeline deploys the same tested image to each client's environment in turn, which keeps every client on a known version.
Step 5: Rollbacks
Because every image is tagged with a commit, rolling back means redeploying the previous tag. Make it one command or one button, and test it before you need it. Keep a few previous images available on the server or in the registry.
The exception is the database. Rolling back code is easy; rolling back a schema change is not, which brings us to migrations.
Step 6: Database migrations
Schema changes should be versioned migrations that run automatically as part of the deploy, using your framework's tool (Entity Framework migrations for .NET, Prisma or similar for Node).
Rules that keep them safe:
- Make migrations backward compatible where possible: add columns before using them, stop using columns before removing them, in separate releases. Then the previous code version still works if you roll back.
- Back up the database before risky migrations.
- Keep long-running data migrations separate from the deploy so they do not block it.
Step 7: Staging, if it earns its keep
A staging environment that mirrors production is valuable when changes need client review or testing with realistic data. The pipeline deploys main to staging automatically, and production after approval or on a tag. For very small projects, good tests plus quick rollbacks can be enough without staging. Decide based on the cost of a bad release.
Step 8: Mobile app builds
Mobile releases benefit from automation even more than servers, because the manual steps (signing, versioning, uploading) are fiddly. Tools like Fastlane or a hosted mobile build service can:
- Build signed iOS and Android binaries from a tag.
- Increment build numbers.
- Upload to TestFlight and the Google Play internal track.
The store review is still manual, but getting a tested build in front of testers becomes one step. For the store side of releases, see publishing your app on the App Store and Google Play.
Step 9: Notifications and visibility
Post deployment results to wherever you will see them: success, failure, which version, who triggered it. Tag error tracking with the release version so you can spot when a deploy introduced a problem. See monitoring and alerts for a small product.
Choosing a CI system
Most hosted Git platforms include a CI service, and for a small team the built-in option is usually the right one: no extra accounts, secrets live next to the code, and pipelines are defined in a file in the repository. Self-hosted runners make sense when builds need access to private networks or when hosted minutes become expensive. Whatever you pick, keep the pipeline definition in the repo so it is versioned and reviewed like code.
Secrets and access
- Store secrets in the CI system's encrypted secrets, scoped to the environments that need them.
- Use a deploy key that can only do what deployment requires.
- Rotate secrets when people leave the project.
- Never print secrets in logs.
Common mistakes
- Pipelines so slow that people bypass them.
- Building on the production server.
- Deployments with no health check, so a broken release looks successful.
- Rollbacks that have never been tried.
- Destructive migrations shipped together with the code that depends on them.
Where to start this week
- Add a pipeline that lints, type checks, tests and builds on every push.
- Build and tag Docker images in CI.
- Automate the deploy with a health check.
- Write down and test the rollback.
Everything else can follow. If you want help setting up CI/CD for your product, see my work and get in touch.