Skip to main content

Docker CI/CD: Automating Your Development Pipeline

Introduction

Continuous Integration and Continuous Deployment (CI/CD) represents one of the most powerful practices in modern software development. When combined with Docker, it creates a robust system for automatically building, testing, and deploying your applications. In this tutorial, we'll explore how to implement a CI/CD pipeline using Docker, making your development process more efficient and reliable.

CI/CD with Docker allows you to:

  • Automatically test code changes
  • Build Docker images consistently
  • Deploy applications with minimal manual intervention
  • Ensure your software is always in a deployable state

Let's dive into the world of Docker CI/CD and transform how you deliver software!

Prerequisites

Before we begin, make sure you have:

  • Basic understanding of Docker concepts (containers, images)
  • Docker and Docker Compose installed on your system
  • A GitHub account (we'll use GitHub Actions for our CI/CD examples)
  • A simple application to containerize (we'll use a basic Node.js app)

Understanding CI/CD Concepts

What is CI/CD?

CI/CD consists of two related but distinct practices:

  1. Continuous Integration (CI): The practice of frequently merging code changes into a shared repository, followed by automated building and testing.

  2. Continuous Deployment/Delivery (CD):

    • Continuous Delivery: Automating the release process up to production, with a manual approval step
    • Continuous Deployment: Fully automating the entire pipeline including deployment to production

Let's visualize the CI/CD pipeline:

Docker's Role in CI/CD

Docker enhances the CI/CD process in several ways:

  • Consistency: Same environment everywhere (development, testing, production)
  • Isolation: Applications and dependencies are packaged together
  • Portability: Docker images can run anywhere Docker is installed
  • Scalability: Easily scale containers based on demand
  • Version Control: Docker images can be versioned like code

Setting Up a Basic CI/CD Pipeline with Docker

Let's create a simple CI/CD pipeline for a Node.js application. We'll use:

  • GitHub for source control
  • GitHub Actions for CI/CD orchestration
  • Docker for containerization
  • Docker Hub for image registry

Step 1: Prepare Your Application

First, let's create a simple Node.js application with a Dockerfile:

// app.js
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;

app.get('/', (req, res) => {
res.send('Hello Docker CI/CD!');
});

app.listen(port, () => {
console.log(`App listening at http://localhost:${port}`);
});
// test.js
const assert = require('assert');
console.log('Running tests...');
assert.strictEqual(1 + 1, 2);
console.log('Tests passed!');
// package.json
{
"name": "docker-cicd-demo",
"version": "1.0.0",
"description": "A simple app to demonstrate Docker CI/CD",
"main": "app.js",
"scripts": {
"start": "node app.js",
"test": "node test.js"
},
"dependencies": {
"express": "^4.17.1"
}
}

Now, create a Dockerfile:

FROM node:14-alpine

WORKDIR /app

COPY package*.json ./

RUN npm install

COPY . .

EXPOSE 3000

CMD ["npm", "start"]

Step 2: Set Up GitHub Actions Workflow

Create a .github/workflows directory in your project and add a docker-ci-cd.yml file:

name: Docker CI/CD

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

jobs:
build-and-test:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v2

- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '14'

- name: Install dependencies
run: npm install

- name: Run tests
run: npm test

- name: Build Docker image
run: docker build -t myapp:${{ github.sha }} .

- name: Test Docker image
run: |
docker run -d -p 3000:3000 --name myapp myapp:${{ github.sha }}
sleep 5
curl http://localhost:3000 | grep "Hello Docker CI/CD"

- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}

- name: Push to Docker Hub
if: github.event_name != 'pull_request'
run: |
docker tag myapp:${{ github.sha }} ${{ secrets.DOCKER_HUB_USERNAME }}/myapp:latest
docker push ${{ secrets.DOCKER_HUB_USERNAME }}/myapp:latest

Step 3: Understanding the Workflow

Let's break down what our GitHub Actions workflow does:

  1. Trigger: The workflow runs on pushes to the main branch and pull requests
  2. Build and Test:
    • Checkout the code
    • Set up Node.js environment
    • Install dependencies
    • Run tests
    • Build a Docker image with a tag based on the commit SHA
    • Start a container from the image and test it
  3. Deploy:
    • Log in to Docker Hub (only for pushes to main, not pull requests)
    • Tag and push the Docker image to Docker Hub

Step 4: Configure Secrets

For our workflow to push to Docker Hub, we need to set up secrets in our GitHub repository:

  1. Go to your GitHub repository
  2. Click on "Settings" > "Secrets" > "New repository secret"
  3. Add:
    • DOCKER_HUB_USERNAME: Your Docker Hub username
    • DOCKER_HUB_TOKEN: Your Docker Hub access token (not your password)

Advanced CI/CD with Docker Compose

For more complex applications with multiple services, we can use Docker Compose in our CI/CD pipeline.

Docker Compose Setup

Create a docker-compose.yml file:

version: '3'

services:
app:
build: .
ports:
- "3000:3000"
environment:
- NODE_ENV=production

db:
image: mongo:4.4
volumes:
- mongo-data:/data/db
ports:
- "27017:27017"

volumes:
mongo-data:

Testing with Docker Compose

Update your GitHub Actions workflow to test with Docker Compose:

# Add this step after building the Docker image
- name: Test with Docker Compose
run: |
docker-compose up -d
sleep 10
curl http://localhost:3000 | grep "Hello Docker CI/CD"
docker-compose down

Implementing Continuous Deployment

Let's extend our pipeline to deploy our application to a server. We'll use SSH to deploy to a remote server:

# Add this job after the build-and-test job
deploy:
needs: build-and-test
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest

steps:
- name: Deploy to production server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.DEPLOY_HOST }}
username: ${{ secrets.DEPLOY_USER }}
key: ${{ secrets.DEPLOY_KEY }}
script: |
cd /path/to/app
docker-compose pull
docker-compose up -d

You'll need to add three more secrets to your GitHub repository:

  • DEPLOY_HOST: Your server's IP address or hostname
  • DEPLOY_USER: SSH username
  • DEPLOY_KEY: SSH private key for authentication

Real-World CI/CD Pipeline Example

Let's put all these concepts together in a complete CI/CD pipeline for a web application with a frontend, backend, and database:

Complete GitHub Actions Workflow

Here's a more comprehensive GitHub Actions workflow file for a real-world application:

name: CI/CD Pipeline

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

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

- name: Set up Node.js
uses: actions/setup-node@v2
with:
node-version: '14'

- name: Install dependencies
run: npm ci

- name: Lint code
run: npm run lint

- name: Run unit tests
run: npm test

build:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2

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

- name: Cache Docker layers
uses: actions/cache@v2
with:
path: /tmp/.buildx-cache
key: ${{ runner.os }}-buildx-${{ github.sha }}
restore-keys: |
${{ runner.os }}-buildx-

- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}

- name: Build and push
uses: docker/build-push-action@v2
with:
context: .
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ secrets.DOCKER_HUB_USERNAME }}/myapp:${{ github.sha }}
cache-from: type=local,src=/tmp/.buildx-cache
cache-to: type=local,dest=/tmp/.buildx-cache-new

# Temp fix for cache size
- name: Move cache
run: |
rm -rf /tmp/.buildx-cache
mv /tmp/.buildx-cache-new /tmp/.buildx-cache

deploy-staging:
needs: build
if: github.ref == 'refs/heads/develop'
runs-on: ubuntu-latest
steps:
- name: Deploy to staging
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.STAGING_HOST }}
username: ${{ secrets.STAGING_USER }}
key: ${{ secrets.STAGING_KEY }}
script: |
cd /path/to/app
docker-compose pull
docker-compose up -d

- name: Run E2E tests
run: |
sleep 30 # Wait for deployment to stabilize
npx cypress run --config baseUrl=https://staging.example.com

deploy-production:
needs: deploy-staging
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment:
name: production
url: https://example.com
steps:
- name: Deploy to production
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.PRODUCTION_HOST }}
username: ${{ secrets.PRODUCTION_USER }}
key: ${{ secrets.PRODUCTION_KEY }}
script: |
cd /path/to/app
docker pull ${{ secrets.DOCKER_HUB_USERNAME }}/myapp:${{ github.sha }}
docker-compose up -d

- name: Health check
run: |
sleep 30
curl -sSf https://example.com/health || exit 1

Best Practices for Docker CI/CD

  1. Use Multi-Stage Builds: Reduce image size and improve security.
# Build stage
FROM node:14-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

# Production stage
FROM node:14-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY package*.json ./
EXPOSE 3000
CMD ["npm", "start"]
  1. Cache Docker Layers: Speed up builds by caching layers.

  2. Tag Images Properly: Use meaningful tags like:

    • Commit SHA: myapp:abc123
    • Git branch: myapp:feature-login
    • Semantic versioning: myapp:1.2.3
    • Latest: myapp:latest
  3. Scan Images for Vulnerabilities:

- name: Scan for vulnerabilities
uses: aquasecurity/trivy-action@master
with:
image-ref: 'myapp:${{ github.sha }}'
format: 'table'
exit-code: '1'
severity: 'CRITICAL,HIGH'
  1. Implement Rollback Strategies: Always have a plan B.

  2. Monitor Your Deployments: Add logging and monitoring.

Common CI/CD Tools for Docker

Several tools can help you implement CI/CD with Docker:

  1. GitHub Actions: Tightly integrated with GitHub repositories
  2. GitLab CI/CD: Built into GitLab with runner support
  3. Jenkins: Highly customizable with many plugins
  4. CircleCI: Cloud-based CI/CD platform with Docker support
  5. Travis CI: Simple configuration for open source projects
  6. DroneCI: Native Docker support with pipeline as code

Practical Exercise: Create Your Own CI/CD Pipeline

Let's practice what we've learned by setting up a CI/CD pipeline for a simple web application:

  1. Create a new GitHub repository
  2. Add a simple web application (Node.js, Python, or any language you prefer)
  3. Add a Dockerfile
  4. Set up GitHub Actions for:
    • Running tests
    • Building a Docker image
    • Pushing to Docker Hub
  5. Optional: Deploy to a free service like Heroku or Render

Summary

In this tutorial, we've covered:

  • CI/CD fundamentals and their importance in modern development
  • Docker's role in creating consistent build and deployment pipelines
  • How to set up a basic CI/CD pipeline with GitHub Actions and Docker
  • Advanced CI/CD techniques with Docker Compose for multi-service applications
  • Best practices for implementing Docker in your CI/CD workflow
  • Common tools for Docker CI/CD implementation

By adopting Docker in your CI/CD pipeline, you can achieve:

  • Faster development cycles
  • More reliable deployments
  • Consistent environments
  • Better collaboration between development and operations

Additional Resources

Next Steps

Now that you understand Docker CI/CD, consider exploring:

  1. Kubernetes for container orchestration
  2. Infrastructure as Code (IaC) tools like Terraform
  3. Advanced monitoring and observability solutions
  4. GitOps workflows for Kubernetes deployments

Happy automating!

💡 Found a typo or mistake? Click "Edit this page" to suggest a correction. Your feedback is greatly appreciated!