How to Set Up a CI/CD Pipeline with GitHub Actions and Docker: A Step-by-Step Guide 2026

How to Set Up a CI/CD Pipeline with GitHub Actions and Docker: A Step-by-Step Guide 2026

Imagine: you commit code, and 5 minutes later it's running on the production server. No manual file copying, no "magic" on the server, and no late-night deployments at 3 AM. Sounds like a dream? In 2026, this is the standard for any self-respecting DevOps team.

A CI/CD pipeline isn't just a buzzword—it's the foundation of modern development. GitHub Actions and Docker have become the perfect pair for automation: the former manages the process, the latter ensures the environment is identical at every stage. In this guide, I'll show you how to build a production-ready pipeline from scratch, using real configs and best practices for 2026.

Why GitHub Actions and Docker Are the Best Combo in 2026

Let's get straight to the point. GitHub Actions is a built-in CI/CD tool in GitHub that doesn't require separate infrastructure. Docker is the containerization standard. Together, they solve the main DevOps problem: "It works on my machine."

Here's what you get:
- Unified runner — GitHub provides free runners (Linux, Windows, macOS) with Docker pre-installed.
- Scalability — you can run parallel jobs for testing on different versions.
- Caching — Docker images are cached, speeding up builds by 40-60%.
- Security — secrets are stored in GitHub Secrets, not in code.

Pipeline Architecture: What This Article Covers

We'll build a pipeline for a typical web application (e.g., Node.js or Python Flask) that:
1. Automatically triggers on a push to the main branch.
2. Builds a Docker image.
3. Runs tests inside the container.
4. Pushes the image to Docker Hub (or GitHub Container Registry).
5. Deploys to a server via SSH.

All code is YAML. No extra software needed.

Step 1: Prepare the Repository and Secrets

The first thing to do is set up security. Never store passwords or tokens in code. Use GitHub Secrets.

What you'll need:
- DOCKER_USERNAME and DOCKER_PASSWORD — for pushing the image to Docker Hub.
- SSH_PRIVATE_KEY — for deploying to the server.
- SERVER_HOST and SERVER_USER — server address and user.

How to add: Settings → Secrets and variables → Actions → New repository secret.

Step 2: Basic Workflow File

Create a file .github/workflows/deploy.yml in the root of your repository. This is the heart of the pipeline.

name: CI/CD Pipeline

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

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: |
          docker build -t myapp:test .
          docker run myapp:test npm test

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - 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 push
        uses: docker/build-push-action@v5
        with:
          push: true
          tags: ${{ secrets.DOCKER_USERNAME }}/myapp:latest

  deploy:
    needs: build-and-push
    runs-on: ubuntu-latest
    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 ${{ secrets.DOCKER_USERNAME }}/myapp:latest
            docker stop myapp || true
            docker rm myapp || true
            docker run -d --name myapp -p 80:3000 ${{ secrets.DOCKER_USERNAME }}/myapp:latest

Expert comment:
- needs — ensures sequence: tests first, then build, then deploy.
- appleboy/ssh-action — a popular action for SSH commands. Alternative: self-hosted runner.
- || true — ignores errors if the container doesn't exist.

Step 3: Dockerfile for Production

Your Dockerfile should be lightweight and secure. Here's an example for a Node.js application:

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

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app .
EXPOSE 3000
CMD ["node", "server.js"]

Why multi-stage?
- Reduces image size by 3-4 times.
- Doesn't include build tools (npm, git) in the final image.
- Smaller attack surface.

Step 4: Optimize Docker Caching

Each pipeline run costs time and money. Caching Docker layers speeds up builds by 50-70%.

Add to build-and-push:

- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3
- name: Build and push with cache
  uses: docker/build-push-action@v5
  with:
    push: true
    tags: ${{ secrets.DOCKER_USERNAME }}/myapp:latest
    cache-from: type=gha
    cache-to: type=gha,mode=max

type=gha uses GitHub Actions cache. Free and efficient.

Step 5: Testing in a Container

Tests should run in an isolated environment identical to production. Use docker-compose for integration tests with a database.

Example .github/workflows/test.yml:

name: Integration Tests
on: [push]

jobs:
  test:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: testpass
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: |
          docker build -t myapp .
          docker run --network host -e DATABASE_URL=postgres://postgres:testpass@localhost:5432/test myapp npm test

Important: Services (PostgreSQL, Redis) run as separate containers accessible via localhost. This is faster than docker-compose.

Step 6: Deploy to AWS EC2 (SSH Alternative)

If your server is AWS EC2, you can use aws-actions/amazon-ecs-deploy-task-definition for deployment to ECS. But for simplicity, we'll stick with SSH.

For production deployment, I recommend adding a Health Check:

script: |
  docker pull ...
  docker run -d --name myapp_new ...
  sleep 10
  curl -f http://localhost:3000/health || exit 1
  docker stop myapp && docker rm myapp
  docker rename myapp_new myapp

This is a minimal blue-green deployment strategy: the new container runs in parallel, is checked, and only then the old one is removed.

Step 7: Pipeline Monitoring

GitHub Actions shows status in the interface, but for serious projects, you need more:
- Slack notifications — action slackapi/slack-github-action.
- Metrics — export build time and deployment frequency to Prometheus.
- Logs — all steps are logged, but for auditing, you can add actions/upload-artifact to save test logs.

Example: Full Pipeline for Python (Flask)

For variety, here's an example for Python with pytest and flake8:

name: Python CI/CD

on: [push]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Lint
        run: |
          docker build --target lint -t myapp:lint .
          docker run myapp:lint flake8

  test:
    needs: lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Test
        run: |
          docker build --target test -t myapp:test .
          docker run myapp:test pytest

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}
      - run: |
          docker build -t myapp:prod --target prod .
          docker tag myapp:prod ${{ secrets.DOCKER_USERNAME }}/flask-app:latest
          docker push ${{ secrets.DOCKER_USERNAME }}/flask-app:latest

Dockerfile with targets:

FROM python:3.12-slim AS base
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt

FROM base AS lint
RUN pip install flake8
COPY . .

FROM base AS test
RUN pip install pytest
COPY . .

FROM base AS prod
COPY . .
CMD ["gunicorn", "app:app"]

Common Mistakes and How to Avoid Them

Mistake Solution
Secrets in code Always use GitHub Secrets. Never hardcode.
Large Docker images Multi-stage build + Alpine.
No caching Use cache-from and cache-to.
Deployment without checks Add Health Check and rollback.
Runners hanging Set a timeout: timeout-minutes: 10.

What's Next? Advanced Techniques

Once the basic pipeline works, you can add:
- Matrix builds — testing on multiple Node/Python versions.
- Self-hosted runners — for deployment to a private network.
- Terraform — automatic infrastructure provisioning before deployment.
- Kaniko — building Docker without Docker daemon (more secure for CI).

Conclusion

A CI/CD pipeline with GitHub Actions and Docker is not a luxury but a necessity for any team that wants to deliver code quickly and without errors. In 2026, the tools have become so mature that you can set up a production-ready pipeline in an evening.

Start small: add automated tests for every commit. Then, add image building. Then, add deployment. In a week, you won't remember how you lived without automation.

Want to master the full DevOps stack? In the course "DevOps and Cloud Technologies," we cover real scenarios: from Docker and Kubernetes to CI/CD with GitHub Actions, Terraform, and Prometheus monitoring. You'll write YAML configs for production, not toy examples. Join us at asibiont.com — and your next deployment will go smoothly without a hitch.

← All posts

Comments