Every time I wanted to update my personal blog, I had to go through a tedious process:
Here’s what my manual deployment looked like:
# on my local machine
docker build -t blog:linux --platform linux/amd64 .
docker save blog:linux | gzip | ssh {user@my-vps} docker load
# ssh to my vps.
ssh {user@my-vps}
# on my vps (remote), i use docker compose for my services
docker compose stop blog
docker compose rm blog
docker compose up -d blog
docker ps -a # check running container
# optional
docker rmi {ID of old image}
This was fine at first, but after doing it multiple times, it became frustrating. I needed automation.
I had heard about GitHub Actions before but never actually used it. Since my blog’s code is already hosted on GitHub, using GitHub Actions for automation made perfect sense.
I started by creating a .github/workflows/github-ci.yml file to automate the deployment process:
name: Deploy Next.js Blog
on:
push:
branches:
- main
jobs:
deploy:
name: Deploy to VPS
runs-on: ubuntu-latest
steps:
# Step 1: Checkout the repository
- name: Checkout code
uses: actions/checkout@v4
# Step 2: Set up Docker
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Step 3: Build the Docker image
- name: Build Docker image
run: |
docker build -t blog:linux .
- name: Configure SSH
env:
VPS_USER: ${{ secrets.VPS_USER }} # Set up secrets for your VPS login
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_SSH_KEY: ${{ secrets.VPS_SSH_KEY }} # Set up an SSH key for authentication
run: |
mkdir -p ~/.ssh/
echo "$VPS_SSH_KEY" > ~/.ssh/vps.key
chmod 600 ~/.ssh/vps.key
cat >>~/.ssh/config <<END
Host vps
HostName $VPS_HOST
User $VPS_USER
IdentityFile ~/.ssh/vps.key
StrictHostKeyChecking no
END
# Step 4: Compress the Docker image and send it over SSH to VPS
- name: Push Docker image to VPS
run: |
# Save and compress the Docker image
docker save blog:linux | gzip | ssh vps docker load
# Step 5: Deploy on VPS using SSH
- name: Deploy to VPS
run: |
ssh vps << 'EOF'
cd ~/projects/
docker compose stop blog
docker compose rm blog -f
docker compose up -d blog
EOF
After committing this workflow, my deployment was fully automated! Every time I pushed to the main branch, my blog would deploy automatically. No more manual SSH-ing!
After setting up automated deployment, I realized that I was only doing CD (Continuous Deployment). To have a proper CI/CD pipeline, I needed to add Continuous Integration (CI) steps before deploying.
I update my previous GitHub Action workflow for additional CI. this is the final workflow .github/workflows/github-ci.yml:
name: CI/CD for Next.js Blog
on:
push:
branches:
- main # Trigger only on pushes to the main branch
pull_request: # Run CI on PRs as well
branches:
- main
jobs:
ci:
name: Continuous Integration
runs-on: ubuntu-latest
steps:
# Step 1: Checkout the repository
- name: Checkout code
uses: actions/checkout@v4
# Step 2: Set up Node.js and pnpm
- name: Set up Node.js and pnpm
uses: pnpm/action-setup@v4
with:
version: 10 # Set pnpm version (adjust if needed)
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
# Step 3: Install dependencies
- name: Install dependencies
run: pnpm install --frozen-lockfile
# Step 4: Run ESLint (Code Linter)
- name: Lint code
run: pnpm run lint
# Step 5: Build Next.js App
- name: Build the app
run: pnpm run build
deploy:
name: Deploy to VPS
needs: ci # Ensure CI passes before deploying
runs-on: ubuntu-latest
steps:
# Step 1: Checkout the repository
- name: Checkout code
uses: actions/checkout@v4
# Step 2: Set up Docker
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v2
# Step 3: Build the Docker image
- name: Build Docker image
run: |
docker build -t blog:linux .
- name: Configure SSH
env:
VPS_USER: ${{ secrets.VPS_USER }} # Set up secrets for your VPS login
VPS_HOST: ${{ secrets.VPS_HOST }}
VPS_SSH_KEY: ${{ secrets.VPS_SSH_KEY }} # Set up an SSH key for authentication
run: |
mkdir -p ~/.ssh/
echo "$VPS_SSH_KEY" > ~/.ssh/vps.key
chmod 600 ~/.ssh/vps.key
cat >>~/.ssh/config <<END
Host vps
HostName $VPS_HOST
User $VPS_USER
IdentityFile ~/.ssh/vps.key
StrictHostKeyChecking no
END
# Step 4: Compress the Docker image and send it over SSH to VPS
- name: Push Docker image to VPS
run: |
# Save and compress the Docker image
docker save blog:linux | gzip | ssh vps docker load
# Step 5: Deploy on VPS using SSH
- name: Deploy to VPS
run: |
ssh vps << 'EOF'
cd ~/projects/
docker compose stop blog
docker compose rm blog -f
docker compose up -d blog
EOF
Now, every time I push code or open a PR, GitHub Actions:
If any of these steps fail, the deployment won’t proceed.
By integrating GitHub Actions, I went from manually deploying my blog to having a fully automated CI/CD pipeline. Now:
✅ Every push to main runs CI checks.
✅ If CI passes, my blog is automatically deployed.
✅ No more SSH-ing and running commands manually.
If you’re deploying a project manually, I highly recommend automating it with GitHub Actions. It saves time, reduces mistakes, and makes your workflow so much smoother.