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.