Toolmingo
Guides9 min read

What is CI/CD? A Complete Guide to Continuous Integration & Deployment

Learn what CI/CD is, how CI/CD pipelines work, and how to set one up with GitHub Actions, GitLab CI, and Jenkins. Includes examples, tool comparison, and best practices.

CI/CD automates the steps between writing code and running it in production. Instead of manual builds, tests, and deploys, a pipeline does it all — every time someone pushes a commit.

CI vs CD: what's the difference?

Three terms get used interchangeably but they mean different things:

Term Full name What it does
CI Continuous Integration Automatically build and test every commit
CD Continuous Delivery Automatically prepare a release-ready artifact
CD Continuous Deployment Automatically deploy to production after tests pass

Continuous Integration is the foundation. Every commit triggers a build and a test suite. If tests fail, the team knows within minutes — before the broken code reaches anyone else.

Continuous Delivery extends CI by packaging the app and making it ready to deploy at any time. A human still presses the deploy button, but the process is fully automated.

Continuous Deployment removes that human step. Every commit that passes all checks ships to production automatically.

Most teams run CI + Continuous Delivery. Full Continuous Deployment requires very high test confidence and feature flags for incomplete work.

CI/CD pipeline stages

A typical pipeline runs these stages in order:

Stage What happens Fails if…
Source Trigger on push / PR
Install Restore dependencies Package not found, lockfile mismatch
Lint Check code style ESLint/flake8/golangci-lint errors
Build Compile or bundle TypeScript errors, missing imports
Test Run unit + integration tests Any test fails
Security scan Check dependencies for CVEs Critical vulnerability found
Build image docker build Dockerfile error
Push image Push to registry Auth failure, disk quota
Deploy staging Apply to staging environment Health check fails
E2E tests Run browser/API tests Critical flow broken
Deploy production Apply to production (gated by approval in CD mode)

Stages run top to bottom. An early failure cancels the rest — no point deploying code that doesn't compile.

GitHub Actions (the most common tool)

GitHub Actions is free for public repos and generous for private ones. Workflows live in .github/workflows/.

Basic Node.js pipeline

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest

    strategy:
      matrix:
        node-version: [18, 20, 22]

    steps:
      - uses: actions/checkout@v4

      - name: Set up Node ${{ matrix.node-version }}
        uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node-version }}
          cache: 'npm'

      - name: Install dependencies
        run: npm ci

      - name: Lint
        run: npm run lint

      - name: Type check
        run: npm run type-check

      - name: Test
        run: npm test -- --coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          token: ${{ secrets.CODECOV_TOKEN }}

Add a deploy job (runs only on main)

  deploy:
    needs: test          # waits for test job to pass
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'

    steps:
      - uses: actions/checkout@v4

      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .

      - name: Log in to registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Push image
        run: |
          docker tag myapp:${{ github.sha }} ghcr.io/${{ github.repository }}:${{ github.sha }}
          docker push ghcr.io/${{ github.repository }}:${{ github.sha }}

      - name: Deploy to production
        run: |
          ssh deploy@${{ secrets.PROD_HOST }} \
            "docker pull ghcr.io/${{ github.repository }}:${{ github.sha }} && \
             docker stop myapp || true && \
             docker run -d --name myapp -p 3000:3000 \
               ghcr.io/${{ github.repository }}:${{ github.sha }}"
        env:
          SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}

Useful GitHub Actions patterns

# Cache dependencies for faster builds
- uses: actions/cache@v4
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}

# Run only when relevant files change
on:
  push:
    paths:
      - 'src/**'
      - 'package.json'
      - 'Dockerfile'

# Set environment variables per job
env:
  NODE_ENV: test
  DATABASE_URL: ${{ secrets.TEST_DATABASE_URL }}

# Manual approval gate before production deploy
- name: Wait for approval
  uses: trstringer/manual-approval@v1
  with:
    secret: ${{ github.TOKEN }}
    approvers: username1,username2

GitLab CI

GitLab CI uses .gitlab-ci.yml in the repo root. Runners execute jobs — GitLab provides shared runners or you can host your own.

# .gitlab-ci.yml
stages:
  - install
  - test
  - build
  - deploy

variables:
  NODE_VERSION: "20"
  DOCKER_DRIVER: overlay2

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - node_modules/

install:
  stage: install
  image: node:${NODE_VERSION}
  script:
    - npm ci

lint_and_test:
  stage: test
  image: node:${NODE_VERSION}
  script:
    - npm run lint
    - npm run type-check
    - npm test -- --coverage --coverageReporters=cobertura
  coverage: '/Lines\s*:\s*(\d+\.?\d*)%/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage/cobertura-coverage.xml

build_image:
  stage: build
  image: docker:24
  services:
    - docker:24-dind
  script:
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  only:
    - main

deploy_production:
  stage: deploy
  script:
    - kubectl set image deployment/myapp myapp=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
  environment:
    name: production
    url: https://myapp.example.com
  when: manual          # requires a human click in GitLab UI
  only:
    - main

Jenkins declarative pipeline

Jenkins uses a Jenkinsfile at the repo root. More complex to set up than GitHub Actions or GitLab CI, but highly flexible for on-premises environments.

// Jenkinsfile
pipeline {
    agent any

    tools {
        nodejs '20'
    }

    environment {
        REGISTRY = 'registry.example.com'
        IMAGE    = "${REGISTRY}/myapp"
    }

    stages {
        stage('Install') {
            steps {
                sh 'npm ci'
            }
        }

        stage('Test') {
            steps {
                sh 'npm test'
            }
            post {
                always {
                    junit 'test-results/*.xml'
                }
            }
        }

        stage('Build image') {
            when { branch 'main' }
            steps {
                sh "docker build -t ${IMAGE}:${GIT_COMMIT} ."
            }
        }

        stage('Deploy') {
            when { branch 'main' }
            steps {
                withCredentials([sshUserPrivateKey(
                    credentialsId: 'deploy-key',
                    keyFileVariable: 'SSH_KEY'
                )]) {
                    sh """
                        ssh -i $SSH_KEY deploy@prod.example.com \
                          'docker pull ${IMAGE}:${GIT_COMMIT} && \
                           docker restart myapp'
                    """
                }
            }
        }
    }

    post {
        failure {
            slackSend(color: 'danger', message: "Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}")
        }
    }
}

CI/CD tools comparison

Tool Host Free tier Config Best for
GitHub Actions Cloud 2,000 min/mo (private) YAML GitHub repos, open source
GitLab CI Cloud / self-hosted 400 min/mo YAML GitLab repos, self-hosted
CircleCI Cloud 6,000 min/mo YAML Fast Docker builds
Jenkins Self-hosted Free Groovy DSL On-premises, complex pipelines
Bitbucket Pipelines Cloud 50 min/mo YAML Atlassian stack
Travis CI Cloud Paid (was free for OSS) YAML Legacy open source
Drone Self-hosted Free YAML Lightweight, container-native
Tekton Self-hosted (k8s) Free CRDs Kubernetes-native pipelines
Argo CD Self-hosted (k8s) Free Declarative GitOps deployments
AWS CodePipeline Cloud (AWS) 1 free pipeline JSON/YAML All-AWS stacks

Rule of thumb:

  • Using GitHub? → GitHub Actions
  • Using GitLab or want self-hosted? → GitLab CI
  • On-premises with complex requirements? → Jenkins
  • Kubernetes with GitOps? → Argo CD + Tekton

Secrets and environment variables

Never hardcode credentials in your pipeline config. Every CI/CD tool has a secrets store:

# GitHub Actions — set in repo Settings → Secrets and variables → Actions
${{ secrets.DATABASE_URL }}
${{ secrets.AWS_ACCESS_KEY_ID }}

# GitLab CI — set in Project Settings → CI/CD → Variables
$DATABASE_URL
$AWS_ACCESS_KEY_ID

# Jenkins — use Credentials binding plugin
withCredentials([string(credentialsId: 'db-url', variable: 'DB_URL')]) {
    sh 'echo $DB_URL'
}

Rotate secrets regularly. Audit who has access. Use short-lived tokens (OIDC with AWS/GCP) instead of long-lived keys where possible.

Branch strategy and when pipelines run

Trigger Typical action
PR/MR opened Run lint + tests
Push to feature branch Run lint + tests
Push to main/master Full pipeline: test → build → deploy staging
Tag v*.*.* created Build release artifact, deploy production
Schedule (nightly) Full test suite, dependency audit
Manual trigger Emergency deploy, rollback

Best practices

Keep pipelines fast. Slow pipelines kill developer flow. Target under 5 minutes for the test stage. Cache dependencies. Run jobs in parallel where possible.

# GitHub Actions: parallel jobs
jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps: [...]

  integration-tests:
    runs-on: ubuntu-latest
    steps: [...]

  lint:
    runs-on: ubuntu-latest
    steps: [...]

Fail fast. Put the quickest checks (lint, type check) before the slowest (E2E tests). Don't waste 10 minutes running Playwright if ESLint already found an error.

Use immutable artifacts. Build the Docker image once, push it with the commit SHA as the tag, then promote that exact image through staging → production. Never rebuild for production.

Test in production-like environments. Use Docker in CI to match your production runtime. If production uses PostgreSQL 16, your test database should too.

Pin your action versions. Use actions/checkout@v4 not actions/checkout@latest to avoid unexpected breaking changes.

Implement rollback. Know how to go back. Keep the previous Docker image in the registry. For Kubernetes, kubectl rollout undo does it in seconds.

Common CI/CD mistakes

Mistake Why it hurts Fix
Secrets in pipeline YAML Exposed in logs / git history Use the tool's secrets store
Not caching dependencies Every run downloads GB of packages Add cache step with lockfile hash
Running tests against production DB Destructive test data Spin up a test DB in the pipeline
No parallel jobs 20-minute pipelines Split test suites, run in parallel
Deploying untested Docker image Build once, test, then promote Tag with commit SHA, never rebuild
No rollback plan Broken deploy takes site down for hours Document and test rollback procedure
Ignoring flaky tests Eventually you ignore all failures Quarantine flaky tests, fix them
Different envs between CI and prod "Works on my machine" × CI Use Docker in CI

FAQ

What's the difference between CI/CD and DevOps? DevOps is a culture and set of practices that includes CI/CD. CI/CD is a specific technical implementation — the automated pipeline. DevOps also covers monitoring, incident response, infrastructure as code, and collaboration between dev and ops teams.

How long should a CI/CD pipeline take? For most apps: lint+test under 5 minutes, full pipeline (including Docker build and staging deploy) under 15 minutes. Longer than that and developers start bypassing it. Optimize with caching and parallel jobs.

Should I use feature flags or feature branches? Both are valid. Feature flags let you ship incomplete code safely (the flag hides it). Feature branches isolate development but require merge management. Trunk-based development with feature flags scales better for large teams.

Can I do CI/CD without Docker? Yes. CI/CD is the process, not the technology. Many pipelines zip and upload binaries, deploy Python with git pull + pip install, or use platform-specific tools (Heroku slug compiler, Vercel build). Docker is popular because it ensures environment consistency, not because it's required.

How do I handle database migrations in CI/CD? Run migrations before starting the new app version. In Kubernetes, use an init container or a pre-deployment job. Always make migrations backward compatible (add columns before removing old ones) so you can roll back the code without rolling back the schema.

What's GitOps? GitOps means using Git as the single source of truth for your infrastructure and deployments. Instead of running kubectl apply in a pipeline, you commit YAML to a Git repo and a tool like Argo CD or Flux detects the change and applies it to the cluster. The running state always matches what's in Git.

Related tools

Keep reading

All Toolmingotools are free & run in your browser

No sign-up, no upload, no watermark. Your files never leave your device.

Browse all tools