Implementing Modern CI/CD Pipelines with GitHub Actions and Docker

Learn how to build a robust CI/CD pipeline using GitHub Actions and Docker, covering automated testing, building, and deployment to Docker Hub and cloud servers.

Implementing Modern CI/CD Pipelines with GitHub Actions and Docker

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

Download checklist

Introduction

In today’s fast‑paced software development world, continuous integration and continuous delivery (CI/CD) are no longer optional—they are essential. Modern CI/CD pipelines automate code testing, building, and deployment, ensuring that every change is validated and delivered quickly and reliably. Two of the most popular tools for building such pipelines are GitHub Actions and Docker. GitHub Actions provides a powerful, integrated automation platform, while Docker ensures consistent environments across development, testing, and production. In this post, we’ll walk through creating a production-grade CI/CD pipeline that builds a Docker image, pushes it to Docker Hub, and deploys it to a remote server—all triggered by a git push to the main branch.

Why GitHub Actions and Docker?

  • GitHub Actions is tightly integrated with GitHub repositories, offering hundreds of pre‑built actions and seamless context (e.g., pull requests, branches, secrets). It is free for public repositories and has generous limits for private ones.
  • Docker packages your application with its dependencies, eliminating “it works on my machine” problems. Containers are lightweight, portable, and can be run on any system that supports Docker.
  • Combined, you get a pipeline that builds, tests, and ships your application with minimal manual effort, increased confidence, and faster feedback loops.

Prerequisites

Before we dive in, ensure you have the following:

  • A GitHub repository with your application code (e.g., a Node.js, Python, or Go app).
  • A Docker Hub account (you can create one for free).
  • A remote server (e.g., a VPS) with Docker installed and SSH access.
  • Basic knowledge of YAML, Docker, and Git.

Step 1: Dockerize Your Application

First, you need a Dockerfile at the root of your project. Here’s a simple example for a Node.js app:

FROM node:18-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .

FROM node:18-alpine
WORKDIR /app
COPY --from=build /app ./
EXPOSE 3000
CMD ["node", "index.js"]

This multi‑stage build reduces final image size and improves security. For other languages, adjust accordingly.

Step 2: Create the GitHub Actions Workflow

In your repository, create a file at .github/workflows/deploy.yml. This workflow will be triggered on every push to the main branch.

Workflow Overview

  1. Checkout the code.
  2. Log in to Docker Hub using stored secrets.
  3. Build the Docker image with a unique tag (e.g., ${{ github.sha }}).
  4. Push the image to Docker Hub.
  5. Deploy to your server via SSH, pulling the new image and restarting the container.

Here is the complete workflow file:

name: Build and Deploy

on:
  push:
    branches: [ main ]

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

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and tag Docker image
        run: |
          docker build -t yourusername/yourapp:${{ github.sha }} .
          docker tag yourusername/yourapp:${{ github.sha }} yourusername/yourapp:latest

      - name: Push Docker image to Docker Hub
        run: |
          docker push yourusername/yourapp:${{ github.sha }}
          docker push yourusername/yourapp:latest

      - name: Deploy to server
        uses: appleboy/ssh-action@v1.0.3
        with:
          host: ${{ secrets.SSH_HOST }}
          username: ${{ secrets.SSH_USER }}
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            docker pull yourusername/yourapp:latest
            docker stop yourapp || true
            docker rm yourapp || true
            docker run -d --name yourapp -p 80:3000 yourusername/yourapp:latest

Important: Replace yourusername/yourapp with your Docker Hub username and repository name. Also, add the following secrets to your repository (Settings → Secrets and variables → Actions):

Want a personalized diagnostic? Complete our free checklist →

Download checklist
  • DOCKER_USERNAME: Your Docker Hub username.
  • DOCKER_PASSWORD: Your Docker Hub password or access token.
  • SSH_HOST: Your server’s IP address.
  • SSH_USER: The SSH user (e.g., root or deploy).
  • SSH_PRIVATE_KEY: Your SSH private key (the public key must be added to the server’s ~/.ssh/authorized_keys).

Step 3: Add Automated Testing

A robust pipeline should include tests before building. Add a test job that runs before the build-and-deploy job. For example, for a Node.js app:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Install dependencies
        run: npm ci
      - name: Run tests
        run: npm test

  build-and-deploy:
    needs: test
    runs-on: ubuntu-latest
    ...

Now, the deployment will only proceed if all tests pass.

Step 4: Optimizing the Pipeline

Caching Dependencies

Caching can significantly speed up your pipeline. Use the actions/cache action to cache Docker layers or npm packages. For Docker, you can also use Docker’s buildx with caching to remote storage.

Using Docker Compose

If your application uses multiple services (e.g., a web server and a database), you can define a docker-compose.yml and use it in your deployment script. This simplifies running multi‑container applications.

Versioning Tags

Instead of always using latest, consider using semantic versioning or Git tags. You can push tags to trigger builds that produce versioned images.

Step 5: Monitoring and Rollbacks

Once deployed, monitor your application. Tools like UptimeRobot, Sentry, or New Relic can alert you to issues. For rollbacks, you can push a previous Docker tag or revert the Git commit.

Additional Resources

Conclusion

By combining GitHub Actions and Docker, you have created a powerful, automated CI/CD pipeline that ensures your application is consistently built, tested, and deployed. This setup reduces manual errors, accelerates release cycles, and frees your team to focus on writing great code. Start small, iterate, and expand your pipeline as your project grows. Happy coding!

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

Download checklist

Related posts