Implementing Modern CI/CD Pipelines with GitHub Actions and Docker

Discover how to build robust, scalable CI/CD pipelines using GitHub Actions and Docker. From core concepts to advanced caching and security best practices, this guide provides actionable steps for modern software delivery.

Data≈
DevOpsDockerK8s

Implementing Modern CI/CD Pipelines with GitHub Actions and Docker

Is your company ready for AI? Download our free checklist →

Download checklist

Introduction

In the fast-paced world of software development, delivering updates quickly and reliably is paramount. Continuous Integration and Continuous Deployment (CI/CD) have become the backbone of modern DevOps practices, enabling teams to automate the build, test, and deployment processes. Among the myriad of tools available, GitHub Actions and Docker stand out for their flexibility, integration, and community support.

According to the 2023 State of DevOps Report, high-performing organizations deploy code 208 times more frequently than low performers. This staggering difference is largely attributed to mature CI/CD pipelines. In this article, we'll explore how to implement modern CI/CD pipelines using GitHub Actions and Docker, covering everything from basic setup to advanced optimization techniques.

Understanding CI/CD and Its Importance

CI/CD is a method of frequently delivering apps to customers by introducing automation into the stages of app development. The main concepts attributed to CI/CD are continuous integration, continuous delivery, and continuous deployment.

  • Continuous Integration (CI): Developers merge their changes back to the main branch frequently. Each merge triggers an automated build and test process, catching integration issues early.
  • Continuous Delivery (CD): Code is automatically built, tested, and prepared for release to production, but the actual deployment is a manual step.
  • Continuous Deployment: Every change that passes all stages of the production pipeline is released to customers automatically, with no human intervention.

Implementing CI/CD reduces manual errors, accelerates feedback loops, and ensures that software is always in a releasable state. GitHub Actions provides the orchestration, while Docker ensures consistency across environments.

Why GitHub Actions and Docker?

GitHub Actions is a powerful CI/CD platform integrated directly with GitHub repositories. It allows you to automate workflows based on events like pushes, pull requests, or issue creation. Docker, on the other hand, containerizes applications, ensuring that they run the same regardless of where they are deployed.

Together, they offer:

  • Seamless Integration: GitHub Actions natively triggers on GitHub events, and Docker images can be built and pushed directly to container registries like GitHub Container Registry (GHCR) or Docker Hub.
  • Scalability: GitHub Actions provides hosted runners with generous compute resources, and Docker containers can be scaled horizontally.
  • Portability: Docker ensures that the environment in CI is identical to production, eliminating the classic "works on my machine" problem.
  • Cost-Effectiveness: GitHub Actions offers 2,000 free minutes per month for private repositories, and self-hosted runners can further reduce costs.

Setting Up a Basic CI/CD Pipeline

Let's walk through creating a simple CI/CD pipeline for a Node.js application using GitHub Actions and Docker.

Prerequisites

  • A GitHub repository with your application code.
  • A Dockerfile in the repository root.
  • (Optional) A container registry account (like Docker Hub or GitHub Container Registry).

Step 1: Create a Workflow File

In your repository, create a directory .github/workflows and add a YAML file, e.g., ci.yml.

name: CI/CD Pipeline

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

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Build Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: false
          tags: myapp:latest

      - name: Run tests in Docker
        run: |
          docker run --rm myapp:latest npm test

This workflow triggers on pushes and pull requests to the main branch. It checks out the code, sets up Docker Buildx (for advanced build features), builds the Docker image, and runs tests inside the container.

Step 2: Add Deployment Job

To deploy automatically, add a deployment job that depends on the build job. Here's an example using a simple SSH deployment:

  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    steps:
      - name: Deploy to server
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SERVER_HOST }}
          username: ${{ secrets.SERVER_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            docker pull myregistry/myapp:latest
            docker stop myapp || true
            docker rm myapp || true
            docker run -d --name myapp -p 8080:8080 myregistry/myapp:latest

This job runs only on pushes to main, uses SSH to connect to your server, pulls the latest image, and runs it.

Best Practices for Optimizing Pipelines

To make your pipelines faster and more reliable, consider the following best practices:

Use Caching

Caching dependencies can drastically reduce build times. For Node.js, you can cache node_modules:

- name: Cache node_modules
  uses: actions/cache@v4
  with:
    path: node_modules
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

For Docker, layer caching can be enabled with BuildKit:

Want a personalized diagnostic? Complete our free checklist →

Download checklist
- name: Build with cache
  uses: docker/build-push-action@v5
  with:
    cache-from: type=gha
    cache-to: type=gha,mode=max

Use Matrix Builds

Matrix builds allow you to test against multiple versions of languages or operating systems simultaneously:

strategy:
  matrix:
    node-version: [18.x, 20.x]
    os: [ubuntu-latest, windows-latest]

This ensures your code works across environments.

Keep Images Small

Use multi-stage builds to keep your Docker images lean. For example:

FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY package*.json ./
RUN npm ci --only=production
EXPOSE 3000
CMD ["node", "dist/index.js"]

This reduces the final image size, speeding up pushes and pulls.

Use Secrets and Environments

Never hardcode credentials. Use GitHub Secrets for sensitive data and environment variables for configuration:

env:
  DATABASE_URL: ${{ secrets.DATABASE_URL }}

For different environments (staging, production), you can use GitHub Environments to add protection rules and secrets.

Advanced Techniques and Real-World Examples

Deploying to Kubernetes

If you're using Kubernetes, you can update deployments with the new image using kubectl:

- name: Deploy to Kubernetes
  run: |
    kubectl set image deployment/myapp myapp=myregistry/myapp:${{ github.sha }}
    kubectl rollout status deployment/myapp

Using GitHub Actions with Docker Compose

For multi-container applications, Docker Compose can be used in CI to set up the entire stack for integration tests:

- name: Run integration tests
  run: |
    docker-compose up -d
    docker-compose run tests

Automating Versioning and Releases

You can automate semantic versioning using tools like semantic-release:

- name: Release
  uses: cycjimmy/semantic-release-action@v4
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

This will automatically bump version numbers and generate changelogs based on commit messages.

Security Considerations

Security is crucial in CI/CD. Here are some key practices:

  • Use Official Images: Always use official or verified Docker images to avoid vulnerabilities.
  • Scan Images: Use tools like Trivy or Snyk to scan images for vulnerabilities before deployment.
  • Least Privilege: Grant only necessary permissions to workflows and use read-only tokens where possible.
  • Pin Versions: Pin action versions to a full commit SHA to prevent supply chain attacks.
  • Separate Environments: Use separate registries and secrets for production.

Example of scanning with Trivy:

- name: Scan image
  uses: aquasecurity/trivy-action@master
  with:
    image-ref: myapp:latest
    format: 'table'
    exit-code: '1'
    ignore-unfixed: true
    severity: 'CRITICAL,HIGH'

Conclusion

Implementing modern CI/CD pipelines with GitHub Actions and Docker is a game-changer for software delivery. It automates repetitive tasks, ensures consistency, and accelerates time-to-market. By following the practices outlined in this article, you can build pipelines that are fast, reliable, and secure.

At Tanok Tech, we specialize in software development and AI consulting. Our team can help you design and implement CI/CD pipelines tailored to your specific needs, whether you're just starting or looking to optimize existing workflows.

Ready to transform your development process? Contact us today for a free consultation and let's build something great together.

Ready for the next step? Evaluate your company with our free checklist →

Download checklist

Related posts