What is CI/CD?
CI/CD (Continuous Integration / Continuous Deployment) automates the process of building, testing, and deploying your application. Docker makes CI/CD pipelines consistent and reproducible because the same container runs everywhere.
CI/CD Pipeline Stages
1. Code Push
→
2. Build Image
→
3. Run Tests
→
4. Push to Registry
→
5. Deploy
GitHub Actions: Build & Push Docker Image
# .github/workflows/docker-build.yml
name: Build and Push Docker Image
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Container Registry
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=sha,prefix=
type=semver,pattern={{version}}
- 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
Complete CI/CD Pipeline: Build, Test, Deploy
# .github/workflows/ci-cd.yml
name: CI/CD Pipeline
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build test image
run: docker build --target test -t myapp:test .
- name: Run tests
run: docker run --rm myapp:test npm test
- name: Run linting
run: docker run --rm myapp:test npm run lint
build-and-push:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v5
with:
push: true
tags: |
username/myapp:latest
username/myapp:${{ github.sha }}
deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
docker pull username/myapp:latest
docker compose -f docker-compose.prod.yml up -d
Dockerfile with Test Stage
# Multi-stage Dockerfile with test stage
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci
FROM deps AS test
COPY . .
RUN npm run lint
RUN npm test
FROM deps AS builder
COPY . .
RUN npm run build
FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=deps /app/node_modules ./node_modules
CMD ["node", "dist/server.js"]
Docker Layer Caching in CI
# Enable caching with GitHub Actions cache
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: myapp:latest
cache-from: type=gha
cache-to: type=gha,mode=max
# Or use registry-based caching
cache-from: type=registry,ref=myapp:cache
cache-to: type=registry,ref=myapp:cache,mode=max
Key Takeaways
- ✅ Automate builds on every push with GitHub Actions
- ✅ Run tests inside containers for consistent results
- ✅ Use multi-stage builds with a test stage for CI
- ✅ Enable layer caching to speed up CI builds significantly