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.
Comments