This guide demonstrates a complete CI/CD pipeline using GitHub Actions that:
Based on the frontend/backend application in 03-Compose/compose-files/2-fe-be/:
project/
├── .github/
│ └── workflows/
│ └── deploy.yml
├── api/
│ ├── Dockerfile
│ ├── package.json
│ └── server.js
├── frontend/
│ ├── Dockerfile
│ └── index.html
└── compose.yml
Create .github/workflows/deploy.yml in your repository root:
name: Build and Deploy
on:
push:
branches:
- main
- develop
pull_request:
branches:
- main
env:
DOCKER_USERNAME: $
REGISTRY: docker.io
jobs:
build-and-push:
name: Build and Push Docker Images
runs-on: ubuntu-latest
steps:
# Checkout the repository code
- name: Checkout code
uses: actions/checkout@v4
# Set up Docker Buildx for multi-platform builds
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Login to Docker Hub
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: $
password: $
# Extract metadata for tagging
- name: Extract metadata
id: meta
run: |
echo "date=$(date +'%Y%m%d-%H%M%S')" >> $GITHUB_OUTPUT
echo "sha_short=$(echo $ | cut -c1-7)" >> $GITHUB_OUTPUT
echo "branch=$(echo ${GITHUB_REF#refs/heads/})" >> $GITHUB_OUTPUT
# Build and push backend image
- name: Build and push Backend
uses: docker/build-push-action@v5
with:
context: ./api
file: ./api/Dockerfile
push: true
tags: |
$/myapp-backend:latest
$/myapp-backend:$
$/myapp-backend:$
cache-from: type=registry,ref=$/myapp-backend:buildcache
cache-to: type=registry,ref=$/myapp-backend:buildcache,mode=max
# Build and push frontend image
- name: Build and push Frontend
uses: docker/build-push-action@v5
with:
context: ./frontend
file: ./frontend/Dockerfile
push: true
tags: |
$/myapp-frontend:latest
$/myapp-frontend:$
$/myapp-frontend:$
cache-from: type=registry,ref=$/myapp-frontend:buildcache
cache-to: type=registry,ref=$/myapp-frontend:buildcache,mode=max
deploy:
name: Deploy to Production Server
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
# Deploy to server via SSH
- name: Deploy to Production Server
uses: appleboy/ssh-action@v1.0.3
with:
host: $
username: $
key: $
port: $
script: |
# Navigate to application directory
cd /opt/myapp || exit 1
# Pull latest images
docker compose pull
# Restart services with zero downtime
docker compose up -d --remove-orphans
# Clean up old images
docker image prune -af --filter "until=72h"
# Show running containers
docker compose ps
on:
push:
branches: [main, develop] # Build on push to these branches
pull_request:
branches: [main] # Build on PR to main
github-actions-token# Generate a new SSH key specifically for GitHub Actions
ssh-keygen -t ed25519 -C "github-actions-deploy" -f ~/.ssh/github_actions_deploy
# This creates two files:
# - github_actions_deploy (private key - for GitHub secrets)
# - github_actions_deploy.pub (public key - for server)
# Copy public key to server
ssh-copy-id -i ~/.ssh/github_actions_deploy.pub user@your-server.com
# Or manually:
# 1. Copy the contents of github_actions_deploy.pub
cat ~/.ssh/github_actions_deploy.pub
# 2. SSH into your server
ssh user@your-server.com
# 3. Add to authorized_keys
mkdir -p ~/.ssh
chmod 700 ~/.ssh
echo "YOUR_PUBLIC_KEY_CONTENT" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
# Test the connection using the private key
ssh -i ~/.ssh/github_actions_deploy user@your-server.com
# If successful, you're ready to proceed
DOCKER_USERNAME
Value: your-dockerhub-username
DOCKER_TOKEN
Value: [paste the Docker Hub access token you generated]
SERVER_HOST
Value: your-server.com (or IP address like 123.45.67.89)
SERVER_USER
Value: ubuntu (or your server username)
SERVER_PORT
Value: 22 (or your custom SSH port)
SSH_PRIVATE_KEY
Value: [paste the contents of your private key]
To get private key contents:
cat ~/.ssh/github_actions_deploy
# Copy the entire output including the BEGIN and END lines
# SSH into your production server
ssh user@your-server.com
# Install Docker and Docker Compose
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
sudo usermod -aG docker $USER
# Install Docker Compose
sudo apt-get update
sudo apt-get install docker-compose-plugin
# Logout and login again for group changes to take effect
exit
ssh user@your-server.com
# Verify installations
docker --version
docker compose version
# Create application directory
sudo mkdir -p /opt/myapp
sudo chown $USER:$USER /opt/myapp
cd /opt/myapp
Create /opt/myapp/docker-compose.yml on your server:
version: '3.8'
services:
backend:
image: your-dockerhub-username/myapp-backend:latest
restart: unless-stopped
ports:
- "3000:3000"
environment:
- PORT=3000
- MONGO_URI=mongodb://mongodb:27017/foodsdb
- NODE_ENV=production
depends_on:
- mongodb
networks:
- app-network
frontend:
image: your-dockerhub-username/myapp-frontend:latest
restart: unless-stopped
ports:
- "80:80"
networks:
- app-network
mongodb:
image: mongo:latest
restart: unless-stopped
volumes:
- dbdata:/data/db
networks:
- app-network
volumes:
dbdata:
networks:
app-network:
driver: bridge
# Login to Docker Hub on the server
docker login
# Pull and start containers
cd /opt/myapp
docker compose pull
docker compose up -d
# Verify everything is running
docker compose ps
docker compose logs -f
Ensure your Dockerfiles follow the workflow structure:
api/Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
frontend/Dockerfile
FROM nginx:alpine
COPY index.html /usr/share/nginx/html/
COPY *.js /usr/share/nginx/html/
COPY *.css /usr/share/nginx/html/
EXPOSE 80
echo “// Updated” » api/server.js
git add . git commit -m “Update backend API” git push origin main
2. **Monitor GitHub Actions**
- Go to your repository → **Actions** tab
- Watch the workflow run in real-time
- Check for any errors in build or deploy steps
3. **Verify on Production Server**
```bash
# SSH to server
ssh user@your-server.com
# Check containers
cd /opt/myapp
docker compose ps
# Check logs
docker compose logs backend
docker compose logs frontend
# Verify new image was pulled
docker images | grep myapp
If deployment fails or has issues:
# SSH to server
ssh user@your-server.com
cd /opt/myapp
# Roll back to previous version by commit SHA
# Update docker-compose.yml to use specific tag
nano docker-compose.yml
# Change: myapp-backend:latest
# To: myapp-backend:abc1234 (previous commit SHA)
# Pull and restart
docker compose pull
docker compose up -d
Add staging environment:
deploy-staging:
name: Deploy to Staging
needs: build-and-push
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
steps:
- name: Deploy to Staging Server
uses: appleboy/ssh-action@v1.0.3
with:
host: $
username: $
key: $
port: $
script: |
cd /opt/myapp-staging
docker compose pull
docker compose up -d
Add Slack or Discord notifications:
- name: Notify Deployment
if: always()
run: |
curl -X POST $ \
-H 'Content-Type: application/json' \
-d '{
"text": "Deployment $: $"
}'
Add health check before completing deployment:
- name: Health Check
run: |
sleep 10
curl -f http://$/health || exit 1
# Check workflow logs in GitHub Actions
# Common issues:
# - Dockerfile syntax errors
# - Missing dependencies in package.json
# - Context path incorrect
# Verify credentials
# Check Docker Hub token hasn't expired
# Ensure repository exists on Docker Hub
# Verify token has write permissions
# SSH to server manually to test
ssh -i ~/.ssh/github_actions_deploy user@server.com
# Check server has enough disk space
df -h
# Check Docker daemon is running
systemctl status docker
# Check compose file syntax
docker compose config
# Force pull fresh images
docker compose pull --no-cache
docker compose up -d --force-recreate
You now have a complete CI/CD pipeline that:
This setup enables true continuous deployment where every commit to main is automatically built, tested, and deployed to production.