Continuous integration and continuous deployment are no longer optional for serious web applications. The gap between teams that deploy multiple times per day with confidence and teams that dread their monthly release window isn’t talent – it’s pipeline maturity. A well-designed CI/CD pipeline transforms deployment from a stressful, error-prone event into a routine, automated process that lets you ship faster while breaking less. This guide covers the practical decisions involved in building that pipeline for web applications.
Designing Your Build and Test Pipeline
A CI pipeline has one job: determine whether a code change is safe to merge and deploy. Every step in the pipeline should serve that goal. Steps that don’t contribute to that judgment are waste.
Start with the basics: install dependencies, compile or transpile code, run linters and formatters, execute tests, and build the deployable artifact. Each step should fail fast – put the fastest checks first so developers get feedback in seconds rather than waiting for a 20-minute test suite to discover a syntax error.
For a typical modern web application, a good pipeline sequence looks like this. First, install dependencies from a lockfile (not the latest resolution – you want deterministic builds). Second, run static analysis: TypeScript type checking, ESLint, Prettier format verification. These catch obvious issues in under a minute. Third, run unit tests. For a well-structured application, these should complete in one to three minutes. Fourth, run integration tests that verify API routes, database queries, and service interactions. Fifth, build the production artifact – the Docker image, the bundled static files, or whatever your deployment target requires. Sixth, run a small set of smoke tests against the built artifact to verify it starts correctly and serves basic requests.
Cache aggressively. Node modules, build caches, Docker layer caches, and test fixtures should persist between pipeline runs. GitHub Actions caches, GitLab CI caches, and CircleCI workspaces all support this. A pipeline that reinstalls 2,000 npm packages on every run is wasting three to five minutes per build.
Parallelize where possible. Unit tests and integration tests can run simultaneously on different runners. Linting and type checking can run in parallel with each other. The build step typically depends on linting and type checking succeeding, so structure your pipeline as a directed acyclic graph rather than a linear sequence.
Related: Continuous Integration and Deployment Best Practices
Branch Strategy and Merge Policies
Your branching strategy determines how code flows from a developer’s machine to production, and it has a direct impact on deployment frequency and integration risk.
Trunk-based development – where developers merge to the main branch at least once per day, typically multiple times – is the highest-performing pattern according to the DORA research. Short-lived feature branches (lasting hours to two days) get rebased onto main, reviewed, and merged quickly. The main branch is always deployable.
This requires two supporting practices. First, feature flags to hide incomplete work from users. A half-finished feature merged behind a flag doesn’t affect production behavior. Tools like LaunchDarkly, Unleash, or even a simple environment variable can manage flags. Second, comprehensive automated testing so that merging to main doesn’t require manual verification.
If trunk-based development feels too aggressive for your team’s current maturity level, use short-lived feature branches with a strict two-day maximum lifetime. Enforce this socially and through tooling – a dashboard showing open PR age creates healthy pressure. Branches older than two days should be either merged or broken into smaller pieces.
Protect the main branch with merge requirements: passing CI pipeline, at least one code review approval, no merge conflicts, and up-to-date with the latest main branch commits (requiring a rebase before merge ensures the CI results are accurate for the actual merged state).
Environment Architecture: Preview, Staging, Production
Most web applications benefit from three environment tiers, each serving a different purpose.
Preview environments (also called ephemeral or per-PR environments) spin up automatically for each pull request. They give reviewers a deployed version of the changes to interact with, not just a code diff to read. Vercel, Netlify, and Railway provide this natively for their platforms. For custom infrastructure, you can create preview environments using Docker Compose on a shared server, Kubernetes namespaces, or lightweight VMs.
Preview environments should use production-like configuration but with isolated databases seeded with test data. They don’t need to be performance-tuned, but they should accurately reflect the application’s behavior. Every PR description should include a link to its preview deployment.
Staging mirrors production as closely as possible: same infrastructure provider, same database engine, same environment variables (with different values), same monitoring. Its purpose is to catch configuration and infrastructure issues that don’t appear in preview environments. Deploy to staging automatically when the main branch is updated. Run your end-to-end test suite against staging after each deployment.
Production receives deployments that have passed staging validation. The deployment mechanism should be identical between staging and production – only the configuration differs. If you deploy to staging with a Docker image and a Helm chart, you deploy to production with the same Docker image and the same Helm chart, just pointed at different infrastructure.
See also: The AI Technology Stack: Models, Frameworks, and Infrastructure Guide
Deployment Strategies: Zero Downtime and Safe Rollbacks
How you roll out code to production determines your blast radius when something goes wrong. The deployment strategy you choose should match your risk tolerance and your application’s architecture.
Rolling deployments replace instances of the old version with the new version one at a time (or in small batches). At any point during the rollout, some instances serve the old version and others serve the new. This works well for stateless web servers behind a load balancer. Kubernetes rolling deployments handle this natively with configurable parameters for max surge (how many extra instances to create during the rollout) and max unavailable (how many instances can be down simultaneously).
Blue-green deployments maintain two identical production environments. One (blue) serves live traffic while the other (green) is updated with the new version. After the green environment passes health checks, traffic switches from blue to green. If the new version has issues, traffic switches back to blue instantly. The downside is cost: you need double the infrastructure during the transition, though cloud platforms make this practical for short deployment windows.
Canary deployments route a small percentage of traffic (typically 1 to 5 percent) to the new version while the majority continues on the old version. If error rates, latency, or other health metrics degrade for the canary cohort, the deployment is automatically rolled back. If metrics remain healthy, traffic gradually shifts to the new version over minutes to hours. This provides the safest rollout for high-traffic applications. AWS App Mesh, Istio, and Argo Rollouts support canary deployments with automated metric analysis.
For all strategies, define health check endpoints that verify the application is genuinely functional, not just that the process is running. A health check should verify database connectivity, cache availability, and any critical external service dependencies. Use readiness probes (is this instance ready to receive traffic?) separately from liveness probes (is this instance alive at all?).
Database Migrations in a CI/CD World
Database schema changes are the trickiest part of continuous deployment because they’re often not backward-compatible. If you deploy new code that expects a column that doesn’t exist yet, or drop a column that the old code still references, you’ll have downtime.
The expand-contract pattern solves this. In the expand phase, you add the new schema elements (new columns, new tables, new indexes) without removing anything. Deploy the code that can work with both the old and new schema. In the contract phase, after all instances are running the new code, remove the old schema elements that are no longer needed.
For example, renaming a column from “username” to “display_name” becomes three steps: (1) add the “display_name” column and populate it from “username” in a migration, (2) deploy code that reads from “display_name” and writes to both columns, (3) after all instances are updated, remove the “username” column and the dual-write logic.
Use a migration tool that tracks which migrations have been applied: Flyway for JVM applications, Alembic for Python, Prisma Migrate or Knex migrations for Node.js, or ActiveRecord migrations for Ruby. Run migrations as part of the deployment pipeline, before the application starts. Never run migrations manually in production – automate them and test them in staging first.
For large tables, be aware that schema changes can lock the table for the duration of the migration. Tools like gh-ost (for MySQL) and pg_repack (for PostgreSQL) perform schema changes without locking by creating a shadow table, syncing data, and swapping tables atomically.
Monitoring, Alerting, and Deployment Observability
A deployment pipeline without monitoring is driving blindfolded. You need to know whether a deployment improved or degraded the application’s behavior, and you need to know quickly.
Instrument your application with three types of telemetry. Metrics track numeric measurements over time: request latency (p50, p95, p99), error rate, throughput, CPU and memory utilization, and database query duration. Use Prometheus, Datadog, or CloudWatch to collect and visualize metrics. Logs record discrete events with context: every error should include a stack trace, request ID, and relevant business context. Centralize logs with Elasticsearch, Loki, or a managed service. Traces follow individual requests across service boundaries, showing where time is spent. Use OpenTelemetry with Jaeger, Zipkin, or a commercial APM like New Relic.
After every deployment, automatically compare key metrics between the pre-deployment and post-deployment windows. If the error rate increases by more than a threshold (say, 2x the baseline) or p95 latency exceeds the SLO, trigger an automated rollback. This “metric-based rollback” is the safety net that makes frequent deployment sustainable.
Set up alerts for conditions that require human attention: sustained error rate above threshold, deployment pipeline failure on the main branch, certificate expiration approaching, disk usage above 80 percent, and any manual rollback. Route alerts to the on-call engineer through PagerDuty, Opsgenie, or your team’s communication channel. Avoid alert fatigue by tuning thresholds aggressively – every alert should be actionable.
A mature CI/CD pipeline is the foundation of reliable, fast software delivery. If your team is still deploying manually or struggling with unreliable pipelines, get in touch and we’ll help you build a deployment workflow that lets you ship with confidence.