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:
- Runs unit tests for my Python Flask API.
- Builds my Astro frontend.
- Deploys only the necessary build output to my VPS.
Let’s dive in.
Tech Stack Overview 🛠️
Here’s what I’m using:
| Purpose | Tool/Tech |
|---|---|
| Website Generator | Astro |
| Backend API | Python + Flask |
| Deployment Target | VPS with Docker + Apache |
| CI/CD Engine | GitHub Actions |
| File Sync | rsync 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
rsyncover SSH to sync only the built/distfolder 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:
- Local Development: Make changes and commit code locally.
- Push to GitHub: Push your changes to the remote repository.
- GitHub Actions: The workflow is triggered automatically.
- Build Frontend: Astro site is built in the CI environment.
- Test API: Python/Flask API tests are run to ensure backend stability.
- Deploy: The built frontend (
distfolder) is securely synced to your VPS usingrsyncover SSH. - 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