How to Set Up a CI/CD Pipeline from Scratch: Docker, GitHub Actions, and Kubernetes

Published 17 min read
CI/CDGitHub ActionsDockerKubernetesDevOpsdeployment

A CI/CD pipeline automates the process of building, testing, and deploying application code from a developer's machine to production. Continuous Integration (CI) automatically builds and tests every code change. Continuous Deployment (CD) automatically deploys validated changes to production. Together, they reduce deployment risk, accelerate feedback loops, and eliminate manual release processes.

The 2025 DORA State of DevOps report found that elite-performing teams deploy 973 times more frequently than low performers, with 6,570 times faster lead time from commit to production. CI/CD is the primary enabler of this performance gap.

What You Will Build

This guide walks through building a complete CI/CD pipeline with:

  • Docker for containerizing the application
  • GitHub Actions for CI/CD automation
  • Kubernetes for production deployment
  • Rolling updates with automatic rollback on failure
  • Staging environment for pre-production validation

Step 1: Containerize the Application with Docker

A Dockerfile defines how your application is packaged into a container image. This example uses a multi-stage build to minimize image size.

<h1>Stage 1: Build</h1>

FROM node:20-alpine AS builder

WORKDIR /app

COPY package*.json ./

RUN npm ci --only=production

COPY . .

RUN npm run build

<h1>Stage 2: Production</h1>

FROM node:20-alpine AS production

WORKDIR /app

RUN addgroup -g 1001 -S appgroup && \

adduser -S appuser -u 1001 -G appgroup

COPY --from=builder /app/dist ./dist

COPY --from=builder /app/node_modules ./node_modules

COPY --from=builder /app/package.json ./package.json

USER appuser

EXPOSE 3000

HEALTHCHECK --interval=30s --timeout=3s --start-period=5s \

CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1

CMD ["node", "dist/server.js"]

Key practices in this Dockerfile:

  • Multi-stage build reduces image size from ~900 MB to ~150 MB by excluding build tools from the production image
  • Non-root user prevents container breakout attacks (76% of containers run as root by default, per Sysdig 2024)
  • HEALTHCHECK enables Kubernetes liveness probe integration
  • npm ci ensures deterministic dependency installation from package-lock.json

Step 2: Write the GitHub Actions CI Workflow

Create `.github/workflows/ci.yml` to run on every push and pull request.

name: CI Pipeline

on:

push:

branches: [main]

pull_request:

branches: [main]

env:

REGISTRY: ghcr.io

IMAGE_NAME: ${{ github.repository }}

jobs:

test:

runs-on: ubuntu-latest

steps:

- uses: actions/checkout@v4

- name: Setup Node.js

uses: actions/setup-node@v4

with:

node-version: 20

cache: npm

- name: Install dependencies

run: npm ci

- name: Run linter

run: npm run lint

- name: Run tests

run: npm test -- --coverage

- name: Upload coverage

uses: codecov/codecov-action@v4

if: github.event_name == 'push'

build:

needs: test

runs-on: ubuntu-latest

if: github.event_name == 'push' && github.ref == 'refs/heads/main'

permissions:

contents: read

packages: write

steps:

- uses: actions/checkout@v4

- name: Log in to Container Registry

uses: docker/login-action@v3

with:

registry: ${{ env.REGISTRY }}

username: ${{ github.actor }}

password: ${{ secrets.GITHUB_TOKEN }}

- name: Build and push image

uses: docker/build-push-action@v5

with:

context: .

push: true

tags: |

${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }}

${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

cache-from: type=gha

cache-to: type=gha,mode=max

This workflow runs tests on every PR and only builds/pushes container images on merges to main. The GitHub Container Registry (ghcr.io) stores images for free for public repositories. Docker layer caching via GitHub Actions cache reduces build times by 50-70% on subsequent runs.

Step 3: Create Kubernetes Deployment Manifests

Define the Kubernetes resources for your application.

apiVersion: apps/v1

kind: Deployment

metadata:

name: web-app

labels:

app: web-app

spec:

replicas: 3

selector:

matchLabels:

app: web-app

strategy:

type: RollingUpdate

rollingUpdate:

maxSurge: 1

maxUnavailable: 0

template:

metadata:

labels:

app: web-app

spec:

containers:

- name: web-app

image: ghcr.io/your-org/your-app:latest

ports:

- containerPort: 3000

resources:

requests:

cpu: 100m

memory: 128Mi

limits:

cpu: 500m

memory: 512Mi

readinessProbe:

httpGet:

path: /health

port: 3000

initialDelaySeconds: 5

periodSeconds: 10

livenessProbe:

httpGet:

path: /health

port: 3000

initialDelaySeconds: 15

periodSeconds: 20

Key deployment settings:

  • maxUnavailable: 0 ensures zero downtime during rolling updates
  • readinessProbe prevents routing traffic to pods that are not ready
  • livenessProbe restarts pods that become unresponsive
  • Resource requests and limits enable proper scheduling and prevent resource starvation

Step 4: Add the CD Workflow for Kubernetes Deployment

Extend the GitHub Actions workflow with a deployment job.

  deploy-staging:

needs: build

runs-on: ubuntu-latest

environment: staging

steps:

- uses: actions/checkout@v4

- name: Configure kubectl

uses: azure/k8s-set-context@v3

with:

kubeconfig: ${{ secrets.KUBE_CONFIG }}

- name: Deploy to staging

run: |

kubectl set image deployment/web-app \

web-app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \

-n staging

kubectl rollout status deployment/web-app -n staging --timeout=300s

deploy-production:

needs: deploy-staging

runs-on: ubuntu-latest

environment: production

steps:

- uses: actions/checkout@v4

- name: Configure kubectl

uses: azure/k8s-set-context@v3

with:

kubeconfig: ${{ secrets.KUBE_CONFIG_PROD }}

- name: Deploy to production

run: |

kubectl set image deployment/web-app \

web-app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \

-n production

kubectl rollout status deployment/web-app -n production --timeout=300s

- name: Verify deployment

run: |

kubectl get pods -n production -l app=web-app

curl -sf https://your-app.com/health || exit 1

The `environment` field enables GitHub environment protection rules: require approval before production deployments, restrict which branches can deploy, and limit deployment frequency.

Step 5: Implement Rollback Strategy

Kubernetes tracks deployment history, enabling instant rollback.

Manual rollback:

<h1>View deployment history</h1>

kubectl rollout history deployment/web-app -n production

<h1>Rollback to the previous version</h1>

kubectl rollout undo deployment/web-app -n production

<h1>Rollback to a specific revision</h1>

kubectl rollout undo deployment/web-app -n production --to-revision=3

Automated rollback can be added to the deployment workflow:

      - name: Deploy with automatic rollback

run: |

kubectl set image deployment/web-app \

web-app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.sha }} \

-n production

if ! kubectl rollout status deployment/web-app -n production --timeout=300s; then

echo "Deployment failed, rolling back..."

kubectl rollout undo deployment/web-app -n production

exit 1

fi

If the rollout does not complete within 5 minutes (pods fail readiness probes), the script automatically rolls back to the previous version and fails the pipeline.

Step 6: Add Post-Deployment Monitoring

After deployment, verify application health by checking key metrics. For a complete monitoring setup, see our infrastructure monitoring guide.

Essential post-deployment checks:

  • HTTP status codes -- Monitor 5xx error rate for the first 10 minutes after deployment
  • Latency -- Compare p50 and p99 latency against pre-deployment baseline
  • Pod restart count -- Any restart within 5 minutes indicates a crash loop
  • Resource usage -- Ensure CPU and memory stay within requested limits

Pipeline Architecture Summary

StageToolTriggerDurationFailure Action

LintESLint/PrettierEvery push30-60 secondsBlock merge
TestJest/VitestEvery push1-5 minutesBlock merge
BuildDockerMerge to main2-5 minutesAlert team
Deploy StagingkubectlAfter build1-3 minutesAlert team
Deploy ProductionkubectlAfter staging (manual approval)1-3 minutesAuto-rollback
Verifycurl + metricsAfter production deploy5-10 minutesAuto-rollback

Total time from merge to verified production deployment: 10-20 minutes. The 2025 DORA report classifies this as elite-level performance. For container orchestration context, see our Kubernetes guide. For securing this pipeline, our security checklist covers CI/CD-specific security controls.

Frequently Asked Questions

How much does GitHub Actions cost?

GitHub Actions is free for public repositories (unlimited minutes). For private repositories, the free tier includes 2,000 minutes/month. Additional minutes cost $0.008/minute for Linux runners. Most small teams stay within the free tier. Self-hosted runners on your own infrastructure have no per-minute cost.

Should I use GitHub Actions or Jenkins?

GitHub Actions is recommended for teams using GitHub. It requires zero infrastructure management, integrates natively with GitHub features (environments, branch protection, CODEOWNERS), and has a vast marketplace of pre-built actions. Jenkins is better for organizations with complex, highly customized pipelines or those not using GitHub for source control.

How do I handle database migrations in CI/CD?

Run database migrations as a separate Kubernetes Job before the application deployment. The Job executes migration scripts, and the deployment only proceeds if the Job succeeds. Never run migrations and application code updates simultaneously -- the old application version must be compatible with the new schema (expand/contract migration pattern).

What is the difference between Continuous Delivery and Continuous Deployment?

Continuous Delivery means every change is automatically built, tested, and staged for production, but a human approves the final deployment. Continuous Deployment means every change that passes all automated checks is automatically deployed to production without human intervention. Most organizations start with Continuous Delivery and graduate to Continuous Deployment as confidence in their test suite grows.

How do I handle secrets in CI/CD pipelines?

Never store secrets in code or environment variables in workflow files. Use GitHub Actions encrypted secrets for CI/CD credentials. For Kubernetes, use external secret stores (HashiCorp Vault, AWS Secrets Manager) with the External Secrets Operator to inject secrets into pods. Rotate all CI/CD secrets at least quarterly.