From Push to Production: Building a CI/CD Pipeline for My Personal Website


In the process of building my personal website, I wanted to go beyond static HTML and CSS. I aimed to create a professional-grade setup that mimics real-world development environments: local development, testing, builds, and finally production deployment. That journey led me into the world of CI/CD.

CI/CD stands for Continuous Integration and Continuous Deployment/Delivery—practices that help you automate software development processes, reduce human error, and ship code faster.

Why CI/CD for a Personal Website? 🤔

Though I’m the sole contributor to my website project, implementing CI/CD made sense for several reasons:

  • It helps solidify best practices.
  • It keeps the deployment process clean and repeatable.
  • It reduces the risk of forgetting steps (like building before deploying).
  • It exposes me to real-world tools and workflows I can apply professionally.

This article outlines how I built a GitHub Actions-based pipeline that:

  1. Runs unit tests for my Python Flask API.
  2. Builds my Astro frontend.
  3. Deploys only the necessary build output to my VPS.

Let’s dive in.


Tech Stack Overview 🛠️

Here’s what I’m using:

PurposeTool/Tech
Website GeneratorAstro
Backend APIPython + Flask
Deployment TargetVPS with Docker + Apache
CI/CD EngineGitHub Actions
File Syncrsync over SSH

The goal was to work locally on my dev environment, then push to GitHub. If the code passes all checks, the GitHub Actions runner builds the frontend and syncs it to the VPS.


Step 1: Project Structure 📁

project-root/
├── .github/workflows/deploy.yml
├── frontend/      # Astro project
├── api/           # Flask API
├── docker-compose.yml

The Astro project lives in its own subfolder, and my Flask API is containerized with Docker Compose. The only folder that actually needs to be deployed is frontend/dist, which is the static build output from Astro.


Step 2: Writing Tests for the API 🧪

Inside api, I wrote a small test suite using Python’s built-in unittest:

# test_launches.py
import unittest
from app import app

class AppTestCase(unittest.TestCase):
    def setUp(self):
        self.app = app.test_client()

    def test_get_launches(self):
        response = self.app.get('/api/launches')
        self.assertEqual(response.status_code, 200)
        self.assertIsInstance(response.get_json(), list)

This allows me to validate that the Flask app and endpoint work as expected before I deploy.


Step 3: GitHub Actions Workflow 🤖

# .github/workflows/deploy.yml
name: Deploy Astro Website

on:
  push:
    branches:
      - main

jobs:
  build-deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v4

      - name: Setup Node
        uses: actions/setup-node@v4
        with:
          node-version: '20'

      - name: Install dependencies
        run: |
          cd frontend
          npm ci

      - name: Build Astro site
        run: |
          cd frontend
          npm run build

      - name: Setup SSH key
        env:
          SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }}
        run: |
          echo "$SSH_PRIVATE_KEY" > key.pem
          chmod 600 key.pem

      - name: Deploy to VPS via rsync
        run: |
          rsync -avz --delete -e "ssh -i key.pem -o StrictHostKeyChecking=no" \
            frontend/dist/ youruser@${{ secrets.VPS_HOST }}:/path/to/website/dist/

This file:

  • Checks out the latest code
  • Installs Astro dependencies
  • Builds the site
  • Uses rsync over SSH to sync only the built /dist folder to the VPS

How rsync Works ⚡

rsync is a powerful and efficient tool for file transfer. Unlike scp, it only copies differences, making it faster for incremental deployments:

rsync -avz source/ user@host:/destination/

In CI/CD, it helps keep the VPS lean and quick without bloating the server with unneeded files.


Diagram: CI/CD Pipeline 🖼️

Here’s how the CI/CD process flows, step by step:

  1. Local Development: Make changes and commit code locally.
  2. Push to GitHub: Push your changes to the remote repository.
  3. GitHub Actions: The workflow is triggered automatically.
  4. Build Frontend: Astro site is built in the CI environment.
  5. Test API: Python/Flask API tests are run to ensure backend stability.
  6. Deploy: The built frontend (dist folder) is securely synced to your VPS using rsync over SSH.
  7. Production: Your VPS serves the latest version of your website to the world.

Thoughts on Secrets 🔒

To keep the pipeline secure, secrets like the SSH private key and VPS IP are never hardcoded. They’re stored safely as GitHub Secrets and accessed in workflows using ${{ secrets.NAME }}.


What I Learned 📚

Implementing CI/CD taught me:

  • How to properly separate concerns in a multi-service repo
  • The value of automation, even for solo projects
  • How to securely connect cloud services with minimal trust

Future Plans 🔮

  • Linting: Run ESLint for Astro and Flake8 for Python
  • Deployment Rollbacks: Automatically revert failed deployments
  • Container Builds: Use GitHub Actions to build and push images to a registry
  • Ansible: Automate VPS provisioning and upgrades

Conclusion 🚀

CI/CD isn’t just for teams—it’s a superpower for solo developers too. It helps you write better code, build safer deployments, and grow your technical confidence. Plus, every step mimics the workflows used in professional DevOps environments.

If you’re hosting your own site and not using CI/CD yet: give it a shot!

Let your next git push take you all the way to production.


Thanks for reading! Questions or feedback? Reach out via the contact form or hit me up on my social networks