What Is CI/CD?
CI/CD stands for Continuous Integration and Continuous Delivery or Continuous Deployment. It is a set of software engineering practices that automate how code changes are tested, built, and released. Instead of manually copying application files to servers or running a long deployment checklist, teams use pipelines that execute repeatable steps whenever code changes are submitted or approved.
A well-designed CI/CD pipeline can detect defects before they reach production, produce reproducible release artifacts, reduce deployment risk, and shorten the time between writing code and delivering it to users. However, automation alone does not guarantee reliable releases. The quality of tests, deployment strategy, environment configuration, database compatibility, and rollback procedures still determines whether a release is safe.
Table of Contents
- CI/CD Fundamentals
- How a CI/CD Pipeline Works
- Automated Testing and Quality Gates
- Deployment Strategies and Rollback
- Production Design Example
- Ready-to-Use Example: FastAPI, Docker, and GitHub Actions
- CI/CD Security and Production Best Practices
- Frequently Asked Questions
- Conclusion
CI/CD Fundamentals
CI/CD combines engineering practices that address two related problems: integrating code changes frequently and releasing those changes safely. These problems become more difficult as teams grow, applications accumulate dependencies, and production environments become more complex.
The three commonly discussed practices are continuous integration, continuous delivery, and continuous deployment. They share automation principles but differ in how far changes move through the release process without manual intervention.
Continuous Integration
Continuous Integration (CI) is the practice of frequently integrating code changes into a shared codebase and validating those changes through automated checks.
For example, when a developer opens a pull request, a CI pipeline may automatically:
- Check out the proposed code changes.
- Install application dependencies.
- Run formatting and linting checks.
- Execute unit and integration tests.
- Perform security and dependency checks.
- Build the application to verify that it can be packaged successfully.
The primary goal is fast feedback. Developers should learn about integration failures while changes are still small and easy to investigate, rather than discovering them after several days of unrelated work have accumulated.
Continuous Delivery vs. Continuous Deployment
Continuous Delivery and Continuous Deployment are related but not identical.
| Practice | Purpose | Production Release |
|---|---|---|
| Continuous Integration | Integrate and validate code changes | Not necessarily included |
| Continuous Delivery | Keep software in a releasable state | Typically requires an explicit release decision |
| Continuous Deployment | Release validated changes automatically | Automatic after required checks pass |
Continuous Delivery does not require a human to perform deployment steps manually. A pipeline may automate everything while still requiring approval before the production release.
Continuous Deployment removes that manual release decision for changes that satisfy the automated release policy. It requires confidence in testing, observability, and recovery mechanisms.
Why CI/CD Matters
Without CI/CD, software releases often depend on manual procedures that are difficult to reproduce. An engineer may build the application locally, upload files to a server, restart processes, and manually verify a few endpoints.
These procedures introduce several risks:
- Environment differences: Builds behave differently on developer machines and production servers.
- Missed tests: Important validation steps are skipped under time pressure.
- Unclear release history: Teams cannot easily identify which source revision is deployed.
- Slow recovery: Rolling back requires manually reconstructing an earlier release.
- Large changes: Infrequent deployments accumulate unrelated changes that are harder to troubleshoot.
CI/CD makes release steps repeatable and observable. It also encourages smaller changes, which are generally easier to review, test, deploy, and revert.
How a CI/CD Pipeline Works
A CI/CD pipeline is an automated workflow composed of jobs and steps. Each job runs in a defined environment, such as a hosted runner, a self-hosted worker, or an isolated container.
Pipelines are commonly configured as code and stored alongside the application. This makes pipeline changes reviewable and allows teams to version build and deployment behavior with the software itself.
Common Pipeline Stages
A typical backend application pipeline includes the following stages:
- Source: Retrieve the exact commit or revision being validated.
- Validate: Check formatting, syntax, and static analysis results.
- Test: Run unit, integration, and selected end-to-end tests.
- Build: Produce a deployable package or container image.
- Scan: Check dependencies and artifacts for known vulnerabilities.
- Publish: Store the artifact in a package or container registry.
- Deploy: Release the artifact to staging or production.
- Verify: Run smoke tests and inspect release health.
Not every pipeline needs every stage. A small static website may have a simpler workflow than a payment-processing service with multiple databases and compliance requirements.
The important property is that each stage has a clear purpose and produces reliable feedback for the next stage.
Build Artifacts and Immutability
A build artifact is the output produced by a build process. Depending on the application, it may be a Python wheel, Java JAR, compiled binary, frontend bundle, or Docker image.
Production pipelines should generally build once and promote the same artifact through environments. Rebuilding separately for staging and production can produce different dependencies or binaries even when the source commit is identical.
Container images are commonly identified by immutable digests. A digest allows deployment systems to reference the exact image content instead of relying on mutable tags such as latest.
For more on container packaging, read What Is Docker?.
Pipeline Triggers and Environments
CI/CD pipelines can run in response to pull requests, commits, tags, scheduled events, or manual dispatch actions.
A practical trigger strategy might be:
- Pull request: Run validation, tests, and a build without production credentials.
- Merge to main: Build and publish an immutable release artifact.
- Staging deployment: Deploy the artifact and execute integration or smoke tests.
- Production release: Promote the tested artifact after automated checks and any required approval.
Environment boundaries matter because a pull request from an untrusted contributor should not automatically receive the credentials required to modify production infrastructure.
Automated Testing and Quality Gates
Automated testing is the foundation of a useful CI/CD pipeline. A pipeline that builds successfully but does not validate application behavior may deploy broken software very efficiently.
Different testing layers detect different classes of defects. The goal is not to run every possible test on every commit, but to provide useful confidence without making feedback unnecessarily slow.
Unit, Integration, and End-to-End Testing
Common testing layers include:
- Unit tests: Validate individual functions, classes, and business rules.
- Integration tests: Validate interactions with databases, queues, external services, and framework components.
- Contract tests: Verify compatibility between service producers and consumers.
- End-to-end tests: Exercise complete application workflows through realistic interfaces.
- Smoke tests: Confirm that a newly deployed release can perform essential operations.
Unit tests are generally fast and suitable for frequent execution. Integration and end-to-end tests provide broader coverage but often require more infrastructure, execution time, and maintenance.
A useful pipeline balances these layers instead of depending entirely on slow browser-based tests or superficial endpoint checks.
Security and Dependency Checks
CI/CD systems often have access to source code, build artifacts, package registries, and deployment credentials. This makes them important security boundaries.
Automated checks may include:
- Static application security testing (SAST).
- Dependency vulnerability scanning.
- Container image scanning.
- Secret detection.
- Infrastructure as Code policy checks.
- Software bill of materials (SBOM) generation.
Security scanners produce findings, not absolute guarantees. Teams need policies for severity thresholds, false positives, exceptions, and remediation timelines.
See API Security Best Practices for Production Systems for related application security controls.
Quality Gates and Release Decisions
A quality gate determines whether a pipeline can continue. Examples include requiring all tests to pass, preventing critical security findings, or blocking deployment when a migration compatibility check fails.
Quality gates should reflect actual release risk. A flaky test that randomly blocks deployments can reduce trust in the pipeline, while a missing database compatibility check can allow serious production failures.
Keep required checks deterministic where possible, isolate unreliable external dependencies, and investigate flaky tests rather than repeatedly rerunning them until they pass.
Deployment Strategies and Rollback
Passing CI does not mean that a release is safe under production traffic. Deployment introduces additional risks related to process startup, network routing, database compatibility, resource capacity, and differences between environments.
Deployment strategies control how a new version replaces the previous version and how traffic is distributed during the transition.
Rolling Deployments
A rolling deployment replaces application instances incrementally rather than stopping the entire service at once.
For example, an API with six replicas may start one new instance, wait for its readiness check to pass, and then terminate an old instance. This continues until all replicas use the new version.
Rolling deployments work well for stateless services with compatible application versions. They still require sufficient capacity, graceful shutdown, and database schemas that support overlapping versions.
Read What Is Rolling Deployment? for the mechanics and trade-offs.
Blue-Green and Canary Deployments
Blue-green deployment maintains two application environments. The new version is prepared in the inactive environment, and traffic is switched when validation succeeds.
Canary deployment gradually exposes a new version to a subset of users or requests while monitoring its behavior. If the new version performs well, traffic increases; otherwise, the rollout can be stopped.
These approaches provide different trade-offs:
| Strategy | Traffic Transition | Main Trade-off |
|---|---|---|
| Rolling | Gradual instance replacement | Old and new versions overlap |
| Blue-green | Switch between environments | Requires additional environment capacity |
| Canary | Gradually increase new-version traffic | Requires traffic control and reliable metrics |
Explore What Is Blue-Green Deployment?, What Is Canary Deployment?, and What Is Zero-Downtime Deployment? for implementation details.
Database Migrations and Backward Compatibility
Database schema changes are among the most common causes of deployment failures. Application rollback does not automatically reverse database migrations.
Suppose version 2 renames a database column while version 1 still expects the original name. During a rolling deployment, both versions may run simultaneously, and the old version can fail immediately after the migration.
A safer approach is the expand-and-contract pattern:
- Add the new schema elements without removing the old ones.
- Deploy application code compatible with both representations.
- Migrate or backfill existing data.
- Switch reads and writes to the new representation.
- Remove obsolete schema elements in a later release.
Backward-compatible schema evolution makes rolling releases and rollbacks significantly safer, especially when multiple application versions may run concurrently.
Production Design Example
Consider a SaaS platform running FastAPI services on Amazon ECS with AWS Fargate. The platform uses PostgreSQL on Amazon RDS, Redis on ElastiCache, and Amazon ECR for container images.
The team deploys several times per day and wants to minimize release risk without requiring engineers to execute manual deployment commands.
Production Pipeline Architecture
The CI/CD system uses GitHub Actions for validation and artifact creation, Amazon ECR for image storage, and Amazon ECS for deployment.
The release process follows these steps:
- A developer opens a pull request with a small application change.
- GitHub Actions runs linting, unit tests, and integration tests.
- After merging, the pipeline builds one Docker image.
- The image is scanned and published to Amazon ECR.
- The pipeline deploys the image digest to staging.
- Smoke tests validate the staging deployment.
- The production workflow promotes the same image digest.
- ECS replaces application tasks according to the configured deployment strategy.
Terraform or CloudFormation manages the underlying infrastructure, while the application release pipeline manages container versions. Separating these responsibilities reduces unnecessary infrastructure changes during routine application releases.
For the orchestration layer, see Amazon ECS Deployment Strategies and Auto Scaling.
Release Controls and Observability
A production release should be evaluated using application behavior, not just whether new containers started successfully.
Useful release signals include:
- HTTP 5xx error rate.
- Request latency, especially p95 and p99.
- Container restart and task failure counts.
- Database connection pool saturation.
- Queue processing delay for background workers.
- Business metrics such as checkout completion rate.
Deployment verification should compare these signals against expected behavior and baseline traffic patterns. A new release may pass its health endpoint while introducing a slow database query that degrades real user requests.
For monitoring design, read Observability Explained: Logs, Metrics, and Traces.
Failure Scenario: A Deployment Passes CI but Breaks Production
Suppose a new API version adds an expensive SQL query. All unit tests pass, and staging tests succeed because the staging database contains only a small dataset.
After production deployment, the query scans millions of rows. Request latency increases, database CPU reaches saturation, and unrelated endpoints begin timing out.
The deployment system can restore the previous application image, but the incident also reveals gaps in performance testing, realistic staging data, and production release monitoring.
A stronger pipeline would combine representative database query tests, query-plan inspection, staged rollout, automated health evaluation, and a documented rollback procedure.
Ready-to-Use Example: FastAPI, Docker, and GitHub Actions
This example creates a small FastAPI service with automated tests, a Dockerfile, and a GitHub Actions pipeline. Pull requests run validation and tests. Commits to the main branch also build and publish a Docker image to GitHub Container Registry (GHCR).
The example produces a deployable artifact but intentionally does not modify a production environment. Production deployment credentials, approval rules, and infrastructure-specific rollout logic should be configured separately.
1. Create the FastAPI Application
Create app.py:
import os
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
VERSION = os.getenv("APP_VERSION", "dev")
class CalculationRequest(BaseModel):
a: int
b: int
@app.get("/health")
async def health():
# Keep the basic health endpoint lightweight.
return {"status": "ok"}
@app.get("/version")
async def version():
# Expose the deployed application version.
return {"version": VERSION}
@app.post("/calculate")
async def calculate(data: CalculationRequest):
# A small deterministic operation for CI tests.
return {"result": data.a + data.b}
The application exposes a health endpoint, a version endpoint, and a simple calculation endpoint. The calculation is intentionally small so the pipeline example can focus on testing and packaging rather than application complexity.
In a real service, readiness checks may need to verify that critical startup initialization has completed, while liveness checks should avoid unnecessarily depending on external systems.
2. Add Automated Tests
Create requirements.txt:
# Application dependencies.
fastapi
uvicorn
# Test dependencies.
pytest
httpx
Create test_app.py:
from fastapi.testclient import TestClient
from app import app
client = TestClient(app)
def test_health():
# Verify that the service reports healthy status.
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
def test_calculate():
# Verify the expected calculation result.
response = client.post(
"/calculate",
json={"a": 10, "b": 25},
)
assert response.status_code == 200
assert response.json() == {"result": 35}
def test_invalid_input():
# Reject values that do not match the schema.
response = client.post(
"/calculate",
json={"a": "invalid", "b": 5},
)
assert response.status_code == 422
These tests cover successful behavior and basic input validation. They are not a complete production test suite, but they demonstrate how automated checks can prevent obvious regressions from reaching the build and release stages.
Run the tests locally:
# Create a virtual environment.
python -m venv .venv
# Activate it on Linux or macOS.
source .venv/bin/activate
# Install dependencies.
python -m pip install -r requirements.txt
# Execute the automated test suite.
python -m pytest -q
3. Create a Docker Image
Create Dockerfile:
# Use a lightweight Python runtime.
FROM python:3.12-slim
# Disable bytecode generation and output buffering.
ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
WORKDIR /app
# Install runtime dependencies first for caching.
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy only the application source.
COPY app.py .
# Run as a non-root user.
RUN groupadd --system app \
&& useradd --system --gid app app
USER app
EXPOSE 8000
# Start the API and handle shutdown signals.
CMD ["uvicorn", "app:app", \
"--host", "0.0.0.0", \
"--port", "8000", \
"--timeout-graceful-shutdown", "25"]
This image installs the dependencies listed in requirements.txt, including the test packages for simplicity. A production image should normally install only runtime dependencies, with test tools kept in a separate development dependency group or build stage.
Create .dockerignore:
# Exclude local Python environments and caches.
.venv/
__pycache__/
*.pyc
# Exclude version-control metadata.
.git/
# Exclude local secrets and test artifacts.
.env
.env.*
.pytest_cache/
Build and run the image locally:
# Build a local container image.
docker build -t fastapi-ci-demo:local .
# Run the application on localhost.
docker run --rm \
-p 127.0.0.1:8000:8000 \
-e APP_VERSION=local \
fastapi-ci-demo:local
# In another terminal, test the service.
curl -i http://127.0.0.1:8000/health
The same Dockerfile can be used in CI to produce an image that is stored in a registry and later deployed by an orchestration platform.
4. Configure the GitHub Actions Pipeline
Create .github/workflows/ci.yml:
name: CI and Container Build
on:
# Validate pull requests targeting main.
pull_request:
branches: [main]
# Build and publish after changes reach main.
push:
branches: [main]
permissions:
# Required to check out repository content.
contents: read
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
cache: pip
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: |
python -m pytest -q
build:
# Build only after the test job succeeds.
needs: test
runs-on: ubuntu-latest
# Publishing is restricted to pushes to main.
if: github.event_name == 'push'
permissions:
contents: read
packages: write
steps:
- name: Check out source
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Normalize image name
id: image
env:
REPOSITORY: ${{ github.repository }}
run: |
# Container repository names must be lowercase.
echo "name=ghcr.io/${REPOSITORY,,}" >> "$GITHUB_OUTPUT"
- name: Build and publish image
id: build
uses: docker/build-push-action@v6
with:
context: .
push: true
# Use the commit SHA as an immutable release label.
tags: ${{ steps.image.outputs.name }}:${{ github.sha }}
# Enable build caching between runs.
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Record published image digest
run: |
# Save the exact image identity in CI logs.
echo "Image digest: ${{ steps.build.outputs.digest }}"
This workflow separates validation from artifact publishing. Pull requests execute the test job, while pushes to the main branch execute both jobs and publish the container image.
For production pipelines, pin third-party GitHub Actions to reviewed commit SHAs rather than relying solely on mutable major-version tags. Also pin application dependencies and base images to support reproducible builds.
The image is published with a commit-SHA tag and its digest is recorded. For deployment, prefer the digest rather than assuming a tag can never be overwritten.
5. Run and Verify the Pipeline
Commit the project files and open a pull request against the main branch.
GitHub Actions should execute the test job and report whether the checks passed. After merging into main, the build job should publish an image to GHCR.
To inspect a published image, use the following commands after authenticating to GHCR if the package is private:
# Set the image reference from your repository.
IMAGE="ghcr.io/OWNER/REPOSITORY"
# Pull the image produced from a specific commit.
docker pull "$IMAGE:COMMIT_SHA"
# Start the published application image.
docker run --rm \
-p 127.0.0.1:8000:8000 \
-e APP_VERSION=production-candidate \
"$IMAGE:COMMIT_SHA"
# In another terminal, verify health.
curl -i http://127.0.0.1:8000/health
# Verify the application version.
curl -i http://127.0.0.1:8000/version
Replace OWNER, REPOSITORY, and COMMIT_SHA with values from the actual GitHub repository and successful workflow run.
This example ends with a published and locally verified container image. A complete continuous delivery pipeline would add staging deployment, environment-specific validation, approval policies, and a production deployment job.
CI/CD Security and Production Best Practices
CI/CD pipelines often have privileges that ordinary application processes do not. They may publish packages, update cloud infrastructure, modify production services, or access deployment credentials.
Protecting the pipeline is therefore part of protecting the production environment itself.
Pipeline Security
Important security practices include:
- Least privilege: Give each job only the permissions it needs.
- Short-lived credentials: Prefer OIDC-based cloud authentication over long-lived access keys.
- Protected environments: Restrict production deployments to approved branches and authorized reviewers.
- Dependency pinning: Pin build tools, actions, and dependencies to reviewed versions or digests.
- Artifact integrity: Record immutable image digests and consider signing or provenance attestations.
- Secret isolation: Prevent untrusted pull-request code from receiving production credentials.
- Runner isolation: Avoid reusing compromised or untrusted build environments.
Secrets should not be printed in build logs or embedded in container images. Sensitive deployment operations should use separate credentials and execution contexts from ordinary pull-request validation.
Common CI/CD Mistakes
Several mistakes repeatedly undermine otherwise useful pipelines:
- Testing only after merging: Defects are discovered too late in the development workflow.
- Rebuilding per environment: Staging and production may receive different artifacts.
- Using mutable image tags: Releases become difficult to reproduce and audit.
- Ignoring database compatibility: Rolling deployments fail when old and new application versions share an incompatible schema.
- Deploying without smoke tests: The pipeline reports success even when essential endpoints are broken.
- Overprivileged CI tokens: A compromised job can modify unrelated repositories or infrastructure.
- No rollback procedure: Recovery requires manual investigation during an outage.
- Excessively slow feedback: Developers bypass checks or accumulate large changes while waiting for results.
Production Checklist
Before enabling automated production releases, verify the following:
- Pull requests run reliable automated tests and required quality checks.
- Release artifacts are immutable and traceable to source commits.
- Staging and production use the same tested artifact.
- Production credentials are isolated and restricted.
- Deployment strategies support readiness checks and graceful shutdown.
- Database changes are compatible with overlapping application versions.
- Post-deployment monitoring detects errors, latency regressions, and unhealthy instances.
- Rollback procedures are documented, tested, and compatible with schema changes.
- Pipeline logs and release history are retained for troubleshooting and auditing.
For additional reliability practices, see Reliability Best Practices for Production Systems.
Frequently Asked Questions
CI/CD is often discussed as a single technology, but it is better understood as a combination of development practices, automated workflows, and release policies.
What Does CI/CD Mean?
CI/CD stands for Continuous Integration and Continuous Delivery or Continuous Deployment. Continuous Integration focuses on frequently validating code changes, while Continuous Delivery and Continuous Deployment focus on preparing and releasing software reliably.
What Is the Difference Between CI and CD?
CI validates changes through automated builds and tests. CD extends the workflow toward releasing software. Continuous Delivery typically retains an explicit production release decision, while Continuous Deployment automatically releases changes that satisfy the configured checks.
Which Tools Are Used for CI/CD?
Popular CI/CD tools include GitHub Actions, GitLab CI/CD, Jenkins, CircleCI, Azure Pipelines, and AWS CodePipeline. Kubernetes environments may also use GitOps tools such as Argo CD or Flux for deployment reconciliation.
The right tool depends on repository hosting, deployment infrastructure, security requirements, team experience, and operational complexity.
Does CI/CD Require Docker?
No. CI/CD pipelines can build and deploy Python packages, Java applications, compiled binaries, static websites, serverless functions, and many other artifacts without Docker.
Docker is useful when applications benefit from portable, consistent container images.
Does CI/CD Require Kubernetes?
No. CI/CD works with virtual machines, serverless platforms, managed container services, static hosting, and Kubernetes clusters.
Kubernetes provides orchestration capabilities, while CI/CD automates the process of validating and releasing application changes. Read What Is Kubernetes? for more details.
Can CI/CD Automatically Roll Back a Deployment?
Yes, if the deployment platform and pipeline support rollback operations and appropriate failure detection. A system may automatically restore a previous application version when health checks or monitored metrics violate configured thresholds.
However, rolling back application code does not automatically undo database migrations, external API changes, or irreversible business operations.
Is CI/CD Useful for Small Teams?
Yes. Even a single-developer project benefits from automated tests, repeatable builds, and reliable deployment procedures.
Small teams should begin with a simple pipeline that validates pull requests and produces reproducible releases, then add more sophisticated deployment controls as application risk and operational requirements increase.
Conclusion
CI/CD makes software delivery more reliable by automating code integration, testing, artifact creation, and deployment workflows. Continuous Integration helps teams discover problems early, while Continuous Delivery and Continuous Deployment reduce the manual work and uncertainty involved in releasing software.
Effective CI/CD is not measured only by how quickly a pipeline finishes. Reliable releases require meaningful tests, immutable artifacts, secure credentials, compatible database changes, safe deployment strategies, and observability that can identify regressions under real production traffic.
Key Takeaway: CI/CD turns software releases into a repeatable engineering process. Build once, test thoroughly, promote the same artifact, deploy progressively when appropriate, and maintain a reliable path to recovery when something goes wrong.
More Articles to Read
Explore these related SysDesPro articles covering deployment automation, infrastructure, containers, and production reliability:
Comments (0)