Modern CI/CD Pipelines with GitHub Actions and Docker: A Complete Implementation Guide
Learn how to build production-grade CI/CD pipelines using GitHub Actions and Docker. From multi-stage builds to zero-downtime rollouts, this guide covers everything you need to ship faster.
Modern CI/CD Pipelines with GitHub Actions and Docker: A Complete Implementation Guide
Is your company ready for AI? Download our free checklist →
Download checklistIntroduction: Why CI/CD Still Matters in 2026
Software teams ship code faster than ever before. According to the DORA 2024 State of DevOps Report, elite performers deploy 973 times more frequently than low performers and recover from incidents 6,576 times faster. The common thread? Mature, automated CI/CD pipelines.
While the tooling landscape has exploded (GitLab CI, Jenkins, CircleCI, Buildkite, Drone), GitHub Actions combined with Docker has become the de facto standard for teams already living in the GitHub ecosystem. With over 518 million developers on GitHub and native Docker support baked into runners, you can go from commit to production in minutes.
In this guide, we'll build a production-grade pipeline from scratch: optimized Docker images, security scanning, automated tests, multi-environment deployments, and zero-downtime rollouts.
The Building Blocks: CI, CD, and Containers
Before diving into implementation, let's clarify the fundamentals:
- Continuous Integration (CI): Every code change is automatically built and tested. Failures surface within minutes, not days.
- Continuous Delivery (CD): Every change that passes CI is automatically packaged and ready for production deployment.
- Continuous Deployment: A superset where every passing change is automatically deployed to production.
- Containers: Lightweight, portable runtime units that package an application with its dependencies, ensuring consistency across environments.
Docker solves the classic "it works on my machine" problem. When you combine Docker's reproducibility with GitHub Actions' event-driven automation, you get pipelines that are both fast and trustworthy.
Why GitHub Actions and Docker Are a Perfect Match
GitHub Actions has quietly become one of the most popular CI/CD platforms. Here's why:
- Zero infrastructure to manage — hosted runners are pre-installed with Docker, Node, Python, Go, and dozens of other tools.
- First-class Docker support — the
docker/build-push-actionand Docker layer caching reduce build times by up to 90%. - Tight GitHub integration — pull requests, issues, releases, and branch protections all trigger workflows natively.
- Massive marketplace — 20,000+ pre-built actions, including official Docker, AWS, Azure, GCP, Kubernetes, and security scanning tools.
- Generous free tier — 2,000 minutes/month for private repos on the free plan.
For Docker specifically, GitHub-hosted runners come with the Docker daemon pre-configured, supporting BuildKit, multi-platform builds (linux/amd64, linux/arm64), and rootless builds.
Project Setup: A Real-World Example
Let's build a Node.js microservice with a complete pipeline. Start by initializing the project:
mkdir my-service && cd my-service
git init
npm init -y
npm install express
Create a simple Express application:
// src/index.js
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
app.get('/api/hello/:name', (req, res) => {
res.json({ message: `Hello, ${req.params.name}!` });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
Writing an Optimized Dockerfile
A modern Dockerfile uses multi-stage builds to keep production images small and secure. Here's a production-ready example:
# syntax=docker/dockerfile:1.7
# ---------- Stage 1: Dependencies ----------
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci --omit=dev
# ---------- Stage 2: Build ----------
FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN --mount=type=cache,target=/root/.npm \
npm ci
COPY . .
RUN npm run build
# ---------- Stage 3: Production ----------
FROM node:20-alpine AS production
WORKDIR /app
# Security: run as non-root
RUN addgroup -g 1001 -S nodejs && \
adduser -S appuser -u 1001
COPY --from=deps --chown=appuser:nodejs /app/node_modules ./node_modules
COPY --from=build --chown=appuser:nodejs /app/dist ./dist
COPY --from=build --chown=appuser:nodejs /app/package.json ./
USER appuser
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget --quiet --tries=1 --spider http://localhost:3000/health || exit 1
CMD ["node", "dist/index.js"]
This produces an image that is typically less than 150 MB (versus 1+ GB for a naive build), runs as a non-root user, and includes a healthcheck.
Dockerignore Matters
A .dockerignore file is essential for keeping build context small and avoiding secret leaks:
.git
.github
node_modules
dist
coverage
.env*
*.md
.DS_Store
This can shrink your build context from hundreds of MB to a few KB, dramatically improving build speed.
Building the GitHub Actions Pipeline
Now for the main event. Create .github/workflows/ci-cd.yml:
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
# ---------- Lint & Test ----------
test:
name: Lint and 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
with:
file: ./coverage/lcov.info
# ---------- Build & Push ----------
build:
name: Build and Push Docker Image
needs: test
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix=sha-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
platforms: linux/amd64,linux/arm64
# ---------- Deploy to Staging ----------
deploy-staging:
name: Deploy to Staging
needs: build
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
environment:
name: staging
url: https://staging.example.com
steps:
- uses: actions/checkout@v4
- name: Deploy to Kubernetes
run: |
echo "${{ secrets.KUBECONFIG_STAGING }}" | base64 -d > kubeconfig
KUBECONFIG=kubeconfig kubectl set image deployment/my-service \
my-service=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }} \
--namespace=staging
KUBECONFIG=kubeconfig kubectl rollout status deployment/my-service -n staging
# ---------- Deploy to Production ----------
deploy-production:
name: Deploy to Production
needs: build
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- uses: actions/checkout@v4
- name: Deploy with blue-green strategy
run: |
echo "${{ secrets.KUBECONFIG_PROD }}" | base64 -d > kubeconfig
KUBECONFIG=kubeconfig ./scripts/blue-green-deploy.sh \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
Caching Strategies That Actually Save Time
Docker layer caching is the single biggest performance win. The cache-from: type=gha option leverages GitHub Actions Cache, which is free for public repos and included for private repos on most plans.
For more aggressive caching, consider using registry cache with a dedicated cache image:
Want a personalized diagnostic? Complete our free checklist →
Download checklist- name: Build and push
uses: docker/buildx-action@v5
with:
cache-from: type=registry,ref=ghcr.io/myorg/my-service:cache
cache-to: type=registry,ref=ghcr.io/myorg/my-service:cache,mode=max
Benchmarks from real projects show:
| Strategy | First Build | Subsequent Builds |
|---|---|---|
| No cache | 4m 12s | 4m 08s |
| GHA cache | 4m 20s | 0m 47s |
| Registry cache | 4m 35s | 0m 31s |
That's an 88% reduction in build time.
Security: Don't Ship Vulnerabilities
A pipeline without security scanning is a liability. Add these critical steps:
1. Image Vulnerability Scanning
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
format: 'sarif'
output: 'trivy-results.sarif'
severity: 'CRITICAL,HIGH'
- name: Upload Trivy scan results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-results.sarif'
2. Secret Scanning
- name: Scan for secrets
uses: trufflesecurity/trufflehog@main
with:
path: ./
base: ${{ github.event.repository.default_branch }}
head: HEAD
3. SBOM Generation
Software Bill of Materials is becoming a compliance requirement. Generate one automatically:
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
format: spdx-json
artifact-name: sbom.spdx.json
4. Image Signing with Cosign
Sign images for supply chain integrity:
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Sign image
env:
COSIGN_KEY: ${{ secrets.COSIGN_KEY }}
COSIGN_PASSWORD: ${{ secrets.COSIGN_PASSWORD }}
run: |
cosign sign --key env://COSIGN_KEY \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}
Testing in the Pipeline
A robust pipeline runs multiple test layers:
test-integration:
name: Integration Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_PASSWORD: testpass
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
redis:
image: redis:7-alpine
ports:
- 6379:6379
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run test:integration
env:
DATABASE_URL: postgres://postgres:testpass@localhost:5432/test
REDIS_URL: redis://localhost:6379
GitHub Actions services spin up companion containers automatically — perfect for integration tests that need databases, caches, or message brokers.
Deployment Strategies
Blue-Green Deployments
Run two identical production environments, switch traffic atomically:
#!/bin/bash
# scripts/blue-green-deploy.sh
set -euo pipefail
NEW_IMAGE=$1
CURRENT=$(kubectl get service my-service -o jsonpath='{.spec.selector.version}')
TARGET=$([ "$CURRENT" = "blue" ] && echo "green" || echo "blue")
echo "Deploying to $TARGET environment"
kubectl set image deployment/my-service-$TARGET \
my-service=$NEW_IMAGE --record
kubectl rollout status deployment/my-service-$TARGET --timeout=5m
# Run smoke tests
./scripts/smoke-tests.sh http://my-service-$TARGET.staging.svc.cluster.local
echo "Switching traffic to $TARGET"
kubectl patch service my-service -p '{"spec":{"selector":{"version":"'"$TARGET"'"}}}'
Canary Releases with Flagger
For more advanced rollouts, use Flagger to automatically shift traffic based on metrics:
apiVersion: flagger.app/v1beta1
kind: Canary
metadata:
name: my-service
spec:
provider: kubernetes
targetRef:
apiVersion: apps/v1
kind: Deployment
name: my-service
progressDeadlineSeconds: 60
metrics:
- name: request-success-rate
thresholdRange:
min: 99
interval: 30s
- name: request-duration
thresholdRange:
max: 500
interval: 30s
analysis:
interval: 30s
threshold: 5
maxWeight: 50
stepWeight: 5
Reusable Workflows: DRY for Your Pipelines
If you maintain multiple services, stop copy-pasting YAML. Create .github/workflows/reusable-build.yml:
name: Reusable Build and Deploy
on:
workflow_call:
inputs:
service_name:
required: true
type: string
environment:
required: true
type: string
default: staging
secrets:
KUBECONFIG:
required: true
jobs:
build:
uses: ./.github/workflows/reusable-build.yml
with:
service_name: ${{ inputs.service_name }}
Then any service can reference it:
jobs:
pipeline:
uses: myorg/.github/.github/workflows/reusable-build.yml@main
with:
service_name: my-service
secrets:
KUBECONFIG: ${{ secrets.KUBECONFIG }}
Common Pitfalls to Avoid
- Not pinning action versions — Always use full SHA or version tags, never
@master. Pinning reduces supply chain risk. - Using
latesttags in production —latestis mutable and breaks reproducibility. Use commit SHAs or semver. - Storing secrets in workflow files — Use GitHub Secrets or an external vault. Never echo them in logs.
- Skipping healthchecks — Without them, Kubernetes can't tell a healthy pod from a broken one.
- Building on every push without context — Use path filters to skip irrelevant changes:
```yaml
on:
push:
paths:
- 'src/**'
- 'Dockerfile'
- '.github/workflows/**'
```
- Ignoring build context size — A bloated build context (e.g., accidentally including
node_modules) can push builds from seconds to minutes.
Monitoring Your Pipelines
A pipeline you don't monitor is a pipeline that quietly breaks. Add observability with:
- GitHub's native analytics — track success rates, duration trends, and flaky tests.
- OpenTelemetry exporters — emit workflow metrics to Datadog, Honeycomb, or Grafana Cloud.
- Slack/Teams notifications — instant alerts on failures using
slackapi/slack-github-action. - Pipeline dashboards — visualize MTTR, deployment frequency, and change failure rate.
Conclusion: Ship Faster, Sleep Better
A well-designed CI/CD pipeline with GitHub Actions and Docker isn't just about automation — it's about confidence. When every commit is built, tested, scanned, and deployed through the same proven process, your team can ship multiple times a day without breaking a sweat.
Start small: get tests running on every PR, then add Docker builds, then security scanning, then advanced deployments. Each step compounds. Within a few months, you'll be operating at elite DORA performance levels.
Ready to modernize your delivery pipeline? Tanok Tech specializes in designing custom CI/CD architectures for teams that need to scale. Whether you're migrating from Jenkins, optimizing slow Docker builds, or implementing zero-downtime deployments for Kubernetes, our engineers can help. Contact Tanok Tech today for a free consultation.
Ready for the next step? Evaluate your company with our free checklist →
Download checklistRelated posts
- AI & ML◈
Apple Unveils 2026 AI Developer Tools: A New Era for On-Device Intelligence
Apple Unveils 2026 AI Developer Tools: A New Era for On-Device Intelligence
Sep 28, 2026
- AI & ML◈
The 7% Problem: Why Companies Are Bleeding Money on AI While Ignoring Their People
The 7% Problem: Why Companies Are Bleeding Money on AI While Ignoring Their People
Sep 27, 2026
- AI & ML◈
Babbage's Steam-Powered Dream: How a 3-Meter Mechanical Mind Foretold Modern AI
Babbage's Steam-Powered Dream: How a 3-Meter Mechanical Mind Foretold Modern AI
Sep 26, 2026