Why Manual Deployments Are Broken
Without CI/CD, a deployment looks like: developer runs tests on their machine (which might be misconfigured), manually builds the app (which might have different libraries than the test environment), and uploads it to a server (which is different from both). Something breaks, and the dev says "it worked on my machine." With CI/CD, every commit automatically runs through the same test environment, build environment, and staging environment as production. If it passes, it deploys. If it fails, it stops. The process is identical every time, the environment is identical every time, and humans can't accidentally skip steps or use the wrong version.
CI/CD is a practice of automating testing, building, and deploying code so changes move from git to production reliably and reproducibly. CI (Continuous Integration) automatically tests code. CD (Continuous Delivery/Deployment) automatically builds and deploys it. A machine does what used to require error-prone manual steps.
graph LR A["Git Commit"] --> B["CI: Automated Tests"] B --> C["Build Stage"] C --> D["Security Scan"] D --> E["CD: Deploy to Staging"] E --> F["Integration Tests"] F --> G["Deploy to Production"] style A fill:#e3f2fd style B fill:#fff9c4 style C fill:#fff9c4 style D fill:#ffe0b2 style E fill:#c8e6c9 style F fill:#fff9c4 style G fill:#c8e6c9The Problem CI/CD Solves
The challenge with manual deployments stems from fundamental environmental inconsistencies that cause unpredictable failures. A developer might write code that passes tests on their laptop running macOS with Python 3.8, but fails on a Linux server running Python 3.9 with different environment variables. Different libraries installed locally don't match production versions. Manual build processes are error-prone, taking about 2 hours per deployment and introducing inconsistency.
With CI/CD, a developer pushes code to GitHub, and an automated pipeline immediately runs tests on a consistent environment, builds the app in a container, scans for vulnerabilities, generates an SBOM, signs the image, and deploys to production. The entire process takes about 5 minutes, requires zero manual steps, and eliminates any chance of forgetting a step.
Continuous Integration (CI): The Testing Stage
CI = Every time code is pushed, automatically:
- Build the application
- Run automated tests
- Check code quality
- Scan for vulnerabilities
- Report results
Example workflow: When a developer opens a Pull Request, GitHub automatically runs a CI pipeline that installs dependencies, runs unit tests (pytest, jest, etc.), runs integration tests, checks code coverage (>80%?), lints code for formatting and best practices, performs security scans for vulnerability detection, and performs type checking (mypy, typescript, etc.). If all checks pass, the merge is ready (green checkmark). If any check fails, the merge is blocked (red X), and the developer must fix the issues before merge is allowed. No human can skip these checks—the machine enforces them.
Why this is secure: You can't accidentally merge broken or vulnerable code.
Continuous Delivery/Deployment (CD): The Release Stage
CD = Every time code is merged, automatically:
- Build a production-ready image
- Run final security checks
- Tag and sign the image
- Push to registry
- Deploy to production (or staging)
Difference between Delivery and Deployment:
Continuous Delivery: Automatically prepare for production (build, sign, push to registry), but require human approval before actually deploying. Continuous Deployment: Automatically push to production with no human approval.
Most teams use Delivery (require human approval), some use Deployment (full automation).
Pipeline Stages: The Flow
A typical pipeline progresses through these stages in sequence. Stage 1 (CHECKOUT) pulls your code from the repository on a temporary clean Linux environment in about 10 seconds. Stage 2 (BUILD) compiles code and runs unit tests using commands like npm test, pytest, or mvn clean test on a temporary clean Linux environment, taking 2-5 minutes; if it fails, the process stops and notifies the developer. Stage 3 (SECURITY SCAN) performs comprehensive vulnerability analysis by scanning all dependencies for known CVEs, scanning the code for embedded secrets like API keys and passwords, and generating a Software Bill of Materials (SBOM). Tools like grype, trivy, snyk, and truffleHog handle these checks on a temporary clean Linux environment, taking 1-3 minutes. This stage can be configured to either stop the pipeline on failure or just warn about findings.
Stage 4 (BUILD IMAGE) creates a production-ready Docker image using the Dockerfile. The image is tagged with both semantic version (v1.2.3) and commit SHA for full traceability. Building runs on a temporary clean Linux environment and takes 2-10 minutes depending on image size.
Stage 5 (PUSH TO REGISTRY) uploads the built image to a container registry like Docker Hub or Google Container Registry. It also pushes to registry.cleanstart.io where the image will be cryptographically signed. This stage runs on a temporary clean Linux environment and takes 1-5 minutes.
Stage 6 (SIGN IMAGE) cryptographically signs the image using cosign, providing tamper-proof proof that the image came from your trusted CI system. Signing takes about 30 seconds.
Stage 7 (DEPLOY TO STAGING) is an optional stage where the signed image is deployed to a staging environment. Smoke tests verify basic functionality and human approval is required before proceeding to production. This stage takes 1-5 minutes.
Approval Gate - A human operator reviews the staging deployment and explicitly approves (clicks "Deploy") to proceed to production, ensuring critical changes have human oversight.
Stage 8 (DEPLOY TO PRODUCTION) deploys the verified image to production servers with health checks and smoke tests. The system can automatically roll back if issues are detected. Deployment takes 2-10 minutes. Once complete, the system shows a success indicator marking the deployment as complete.
Why CI/CD Matters for Security
Automated gates prevent bad code from being deployed
When a pull request enters the CI pipeline, security checks automatically run. If vulnerabilities are detected, the merge is blocked and developers must fix issues before proceeding. Humans cannot skip these checks—they are enforced by the machine.
Consistent environment prevents "works on my machine" problems
A developer's laptop may run MacOS with Python 3.8, custom environment variables, and locally installed packages, which means it might work or might not. In contrast, the CI/CD pipeline runs on a consistent Ubuntu 22.04 environment with exactly specified dependencies and a fresh environment created for every run. If code passes here, it will pass in production.
Audit trail provides complete record of who changed what and when
For any container image running in production, you can trace its entire history. When you see an image identifier like myapp@sha256:a1b2c3, you can look up the CI/CD logs to answer critical questions: Who pushed this? When? What code changed? The logs document the exact commit (7f8e9d0 "Fix login bug"), the author (alice@company.com), the timestamp (2024-03-20 14:32:00 UTC), whether all checks passed, that the image was signed by the CI system using cosign, and when the CD system deployed it (15:00:00 UTC). This creates a perfect audit trail for compliance and debugging.
Rollback is easy and fast
If production experiences issues from a deployment, you can quickly recover. Rather than attempting manual fixes, simply re-run the CD pipeline with the previous version. Because the previous version was already built, signed, and tested, it deploys in minutes rather than hours.
Popular CI/CD Platforms
GitHub Actions (best for GitHub projects)
# .github/workflows/deploy.ymlname: Build and Deployon: push: branches: [main]jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: docker/setup-buildx-action@v2 - name: Build run: docker build -t myapp:latest . - name: Test run: docker run myapp:latest npm testGitLab CI (best for GitLab projects)
# .gitlab-ci.ymlstages: - build - test - deploy build: stage: build script: - docker build -t myapp:latest . artifacts: paths: - build/Jenkins (self-hosted, most flexible)
// Jenkinsfilepipeline { agent any stages { stage('Build') { steps { sh 'docker build -t myapp:latest .' } } stage('Test') { steps { sh 'docker run myapp:latest npm test' } } }}Google Cloud Build (best for GCP)
# cloudbuild.yamlsteps: - name: 'gcr.io/cloud-builders/docker' args: ['build', '-t', 'gcr.io/$PROJECT_ID/myapp:latest', '.'] - name: 'gcr.io/cloud-builders/docker' args: ['push', 'gcr.io/$PROJECT_ID/myapp:latest']Real-World Example: GitHub Actions Pipeline
Here's a complete, production-ready pipeline:
name: Build, Test, and Deploy on: push: branches: [main, develop] tags: ['v*'] env: REGISTRY: ghcr.io IMAGE_NAME: ${{ github.repository }} jobs: build-and-test: runs-on: ubuntu-latest permissions: contents: read packages: write security-events: write steps: # Step 1: Checkout code - name: Checkout code uses: actions/checkout@v3 # Step 2: Build Docker image - name: Build Docker image uses: docker/build-push-action@v4 with: context: . push: false tags: test-image:latest outputs: type=docker,dest=/tmp/image.tar # Step 3: Run tests - name: Run tests run: | docker run test-image:latest npm test docker run test-image:latest npm run lint # Step 4: Scan for vulnerabilities - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: input: '/tmp/image.tar' format: 'sarif' output: 'trivy-results.sarif' severity: 'CRITICAL,HIGH' # Step 5: Upload scan results - name: Upload Trivy results to GitHub Security uses: github/codeql-action/upload-sarif@v2 with: sarif_file: 'trivy-results.sarif' # Step 6: Generate SBOM - name: Generate SBOM run: | curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin syft test-image:latest --output spdx-json > sbom.json cat sbom.json # Step 7: Login to registry - name: Login to Container Registry uses: docker/login-action@v2 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} # Step 8: Build and push image - name: Build and push uses: docker/build-push-action@v4 with: context: . push: true tags: | ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} labels: | org.opencontainers.image.revision=${{ github.sha }} org.opencontainers.image.created=$(date -u +'%Y-%m-%dT%H:%M:%SZ') # Step 9: Sign image with Cosign - name: Sign image env: COSIGN_EXPERIMENTAL: 1 run: | curl -sSL https://github.com/sigstore/cosign/releases/download/v2.0.0/cosign-linux-amd64 -o cosign chmod +x cosign ./cosign sign --yes ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} deploy: needs: build-and-test runs-on: ubuntu-latest if: github.ref == 'refs/heads/main' steps: - name: Deploy to production run: | echo "Deploying ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} to production" # Your deploy script here (kubectl, docker-compose, etc.)What this pipeline does:
The pipeline checks out your code, builds a Docker image, runs tests, scans for vulnerabilities, generates a software bill of materials (SBOM), pushes the image to a registry, signs the image, and deploys to production. All of these steps happen automatically with no manual intervention, eliminating any chance of forgetting a security check.
In Practice
- CI = Continuous Integration = Automated testing and building
- CD = Continuous Delivery/Deployment = Automated release and deployment
- Pipeline stages = Checkout → Build → Test → Security → Image → Push → Sign → Deploy
- Automated gates prevent bad code = No manual skipping, reproducible every time
- Audit trail = compliance = Complete record of who/what/when for every deployment
Next Steps
Read End-to-End Secure Deployment to see CI/CD in practice. Read What is a Container Image? for Dockerfile basics. Try Docker-Compose Examples for local testing.
Common mistakes to avoid: Skipping security checks with --skip-checks flags, Running untrusted code in the pipeline (always pin versions), Storing secrets in version control (use secrets management), Not testing the deployment step (failures happen in production, not CI), and Deploying unscanned images to production.
