## Introduction
In this tutorial, I'll show you how to set up a complete CI/CD pipeline that automatically builds Docker images, pushes them to Docker Hub, and deploys your application to a production server - all triggered by a simple git push!
## What You'll Learn
- Setting up GitHub Actions for automated builds
- Building and pushing Docker images to Docker Hub
- Deploying containerized applications to a remote server
- Version tagging with Git commit SHAs
- Zero-downtime deployments with Docker Compose
## Prerequisites
Before we start, make sure you have:
- A GitHub repository with your application code
- A Docker Hub account
- A remote server (VPS/Cloud VM) with Docker and Docker Compose installed
- SSH access to your server
- Basic understanding of Docker and Docker Compose
## Architecture Overview
Our CI/CD pipeline consists of two main jobs:
- Build & Push: Build Docker images and push them to Docker Hub
- Deploy: Pull the new images on the server and restart containers
Git Push → GitHub Actions → Build Images → Docker Hub → Deploy to Server → Live!
## Step 1: Project Structure
Here's what your project structure should look like:
your-project/
├── .github/
│ └── workflows/
│ └── deploy.yml # CI/CD workflow
├── backend/
│ ├── Dockerfile # Backend Docker image
│ └── ... (your backend code)
├── frontend/
│ ├── Dockerfile # Frontend Docker image
│ └── ... (your frontend code)
├── docker-compose.prod.yml # Production deployment config
└── nginx.conf # Nginx configuration
## Step 2: Create Dockerfiles
### Backend Dockerfile Example
# backend/Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 5000
CMD ["node", "server.js"]
*### Frontend Dockerfile Example
*
# frontend/Dockerfile
FROM node:18-alpine as build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
yaml
## Step 3: Create Docker Compose Production File
# docker-compose.prod.yml
version: '3.8'
services:
backend:
image: yourdockerhub/crm-backend:${CRM_BACKEND_TAG:-latest}
container_name: crm-backend
restart: unless-stopped
environment:
- NODE_ENV=production
- DATABASE_URL=${DATABASE_URL}
ports:
- "5000:5000"
networks:
- app-network
frontend:
image: yourdockerhub/crm-frontend:${CRM_FRONTEND_TAG:-latest}
container_name: crm-frontend
restart: unless-stopped
ports:
- "3000:80"
depends_on:
- backend
networks:
- app-network
nginx:
image: nginx:alpine
container_name: nginx-proxy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- frontend
- backend
networks:
- app-network
networks:
app-network:
driver: bridge
## Step 4: Create GitHub Actions Workflow
Create .github/workflows/deploy.yml:
name: Build and Deploy to Production
on:
push:
branches: [main]
jobs:
build-and-push:
name: Build and push Docker images
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Generate image tags
id: tag
run: |
echo "full=${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
echo "short=${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT"
- name: Build and push backend image
uses: docker/build-push-action@v5
with:
context: ./backend
file: ./backend/Dockerfile
push: true
tags: |
yourdockerhub/crm-backend:latest
yourdockerhub/crm-backend:${{ steps.tag.outputs.full }}
yourdockerhub/crm-backend:${{ steps.tag.outputs.short }}
- name: Build and push frontend image
uses: docker/build-push-action@v5
with:
context: ./frontend
file: ./frontend/Dockerfile
push: true
tags: |
yourdockerhub/crm-frontend:latest
yourdockerhub/crm-frontend:${{ steps.tag.outputs.full }}
yourdockerhub/crm-frontend:${{ steps.tag.outputs.short }}
deploy:
name: Deploy to production server
runs-on: ubuntu-latest
needs: build-and-push
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Copy configuration files to server
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
source: "docker-compose.prod.yml,nginx.conf"
target: "~/crm-app"
- name: Deploy application
uses: appleboy/ssh-action@v1.0.0
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_KEY }}
script: |
cd ~/crm-app
TAG="${{ github.sha }}"
# Clean up old images
sudo docker image prune -a -f
# Pull new images
sudo docker compose -f docker-compose.prod.yml pull backend frontend
# Recreate containers with new images
sudo CRM_BACKEND_TAG="$TAG" CRM_FRONTEND_TAG="$TAG" \
docker compose -f docker-compose.prod.yml up -d \
--force-recreate --no-deps backend frontend
# Restart nginx
sudo docker compose -f docker-compose.prod.yml restart nginx
# Final cleanup
sudo docker image prune -a -f
## Step 5: Configure GitHub Secrets
Go to your GitHub repository → Settings → Secrets and variables → Actions
Add the following secrets:
- DOCKERHUB_USERNAME: Your Docker Hub username
- DOCKERHUB_TOKEN: Docker Hub access token
- SSH_HOST: Your server IP address or domain
- SSH_USER: SSH username
- SSH_KEY: Your private SSH key
## Conclusion
You now have a fully automated CI/CD pipeline! Every push to main will automatically build, test, and deploy your application.
Tags: #docker #cicd #githubactions #devops #automation
Questions? Drop them in the comments below! 👇
Top comments (0)