Back to articles
Technology Insight

Build an Automated CI/CD Pipeline for Side Projects: GitHub Actions, Docker, and VPS (No Kubernetes Required)

May 18, 2026

Why Your Side Project Deserves a Professional Deployment Pipeline

Every developer has a graveyard of side projects—ideas that spark, code that gets written, and deployments that become a manual, error-prone chore. The initial excitement fades when you face the repetitive task of SSH-ing into a server, pulling code, installing dependencies, and restarting services. This friction is the primary reason many promising projects never see the light of day or fail to iterate quickly.

This guide is designed to break that cycle. We will construct a fully automated Continuous Integration and Continuous Deployment (CI/CD) pipeline using tools that are powerful, industry-standard, and, crucially, free or very low-cost. By leveraging GitHub Actions for automation, Docker for consistent environments, and a standard Virtual Private Server (VPS) for hosting, we can create a system that rivals the deployment sophistication of large tech companies—without the complexity of orchestration platforms like Kubernetes.

Architecture Overview: A Simple Yet Powerful Stack

Our pipeline will follow a clear, event-driven workflow. Understanding this flow is key to appreciating its simplicity and robustness.

  1. Push to Main Branch: The developer pushes code to the main (or master) branch of the GitHub repository.
  2. GitHub Actions Trigger: This push event automatically triggers a GitHub Actions workflow.
  3. Build and Test Phase (CI): The workflow spins up a virtual machine, checks out the code, runs tests (linting, unit tests), and builds a production-ready Docker image.
  4. Push to Registry: The successfully built Docker image is tagged and pushed to a container registry. We will use GitHub Container Registry (GHCR) as it's integrated and free.
  5. Deploy to VPS (CD): The workflow then securely connects to your VPS via SSH, pulls the new Docker image, and swaps it with the currently running container, ensuring zero-downtime.
  6. Cleanup: Old Docker images are pruned to save disk space on the VPS.

This architecture eliminates manual steps, ensures every deployment is consistent, and integrates testing as a mandatory gatekeeper.

Phase 1: Preparing Your VPS and Docker Environment

Before we write a single line of pipeline code, we must prepare our production environment. For this guide, we assume you have a basic VPS from a provider like DigitalOcean, Linode, or AWS EC2.

Initial Server Setup

Connect to your server and execute the following foundational steps:

  • Create a Deployment User: For security, avoid using the root user. Create a dedicated user (e.g., deployer) with sudo privileges.
  • Install Docker: Follow the official Docker installation guide for your server's OS (Ubuntu, Debian, etc.). This is a one-time setup.
  • Configure Docker for Non-Root User (Optional but Recommended): Add your deployment user to the docker group to run Docker commands without sudo.
  • Open Necessary Ports: Ensure your firewall (e.g., UFW) allows traffic on the port your application will use (e.g., 80, 443, 3000).

Creating a Simple Dockerfile

The heart of our consistent environment is the Dockerfile. It defines exactly how to build your application. Below is a generic example for a Node.js application, but the principles apply to any stack (Python, Go, Java).

Example Dockerfile:
# Use a specific, slim version of the base image for reproducibility
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production

FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY . .

# Expose the application port
EXPOSE 3000

# Define the command to run the app
USER node
CMD ["node", "server.js"]

This multi-stage build keeps the final image lean by separating build dependencies from runtime dependencies.

Phase 2: Crafting the GitHub Actions Workflow

GitHub Actions workflows are defined as YAML files in your repository's .github/workflows/ directory. This file is the blueprint of our automation.

Key Sections of the Workflow File

Let's break down the essential components of our ci-cd-pipeline.yml.

1. Trigger: We define what events start the pipeline. We want it to run on every push to the main branch.

2. Jobs and Steps: A job is a set of steps that execute on the same runner (virtual machine). We will have one primary job with distinct phases.

  • Checkout: Uses the actions/checkout@v4 action to get your code into the runner.
  • Setup Node/Python/etc.: Configures the language environment if needed for running tests.
  • Run Tests: Executes your test suite (e.g., npm test). The pipeline will fail here if tests do not pass, preventing buggy code from being deployed.
  • Log in to Container Registry: Authenticates with GHCR using the automatically available GITHUB_TOKEN.
  • Build and Push Docker Image: Builds the image, tags it with the Git commit SHA (for unique identification) and latest, then pushes it to GHCR.
  • Deploy to VPS: This is the critical CD step. We use the appleboy/ssh-action@v1 action to execute a remote deployment script on your VPS.

The Deployment Script on the VPS

The SSH action doesn't run complex commands directly. Instead, it's best to execute a script file that lives on your VPS. This script encapsulates the deployment logic.

Example ~/deploy.sh on VPS:
#!/bin/bash
set -e # Exit on any error
IMAGE_NAME="ghcr.io/your-username/your-repo"
cd /path/to/your/app/directory || exit

echo "Pulling the latest Docker image..."
docker pull $IMAGE_NAME:latest

echo "Stopping the old container..."
docker stop your-app-name || true
docker rm your-app-name || true

echo "Starting the new container..."
docker run -d \
--name your-app-name \
--restart unless-stopped \
-p 3000:3000 \
-e NODE_ENV=production \
$IMAGE_NAME:latest

echo "Cleaning up old images..."
docker image prune -af

This script performs a rolling update: it pulls the new image, stops and removes the old container, and starts a new one with the same name and configuration. The --restart unless-stopped flag ensures the app restarts if the server reboots.

Phase 3: Securing the Connection with SSH and Secrets

The most sensitive part of the pipeline is the SSH connection. We must never store credentials in our code.

Setting Up SSH Authentication

  1. Generate a Deployment Key Pair: On your VPS, as the deployer user, run ssh-keygen -t ed25519. Do not set a passphrase (as it needs to be used non-interactively by the pipeline).
  2. Add Public Key to VPS: Add the generated public key (~/.ssh/id_ed25519.pub) to the ~/.ssh/authorized_keys file of the deployer user on the VPS.
  3. Add Private Key to GitHub Secrets: Copy the private key content. In your GitHub repository, go to Settings > Secrets and variables > Actions. Create a new secret named VPS_SSH_KEY and paste the private key.
  4. Add Other Secrets: Also create secrets for VPS_HOST (your server's IP or domain), VPS_USERNAME (e.g., deployer), and GHCR_TOKEN (you can use GITHUB_TOKEN for GHCR).

The workflow YAML will reference these secrets using the ${{ secrets.SECRET_NAME }} syntax, keeping them secure.

Advanced Considerations and Best Practices

Once the basic pipeline is operational, consider these enhancements to improve reliability and professionalism.

Implementing Zero-Downtime Deployments

The simple script provided causes a few seconds of downtime between stopping the old container and starting the new one. For true zero-downtime, you can use Docker in conjunction with a reverse proxy like nginx or Caddy. The pattern involves:

  • Running the new container on a temporary internal port.
  • Health-checking the new container until it's ready.
  • Instructing the reverse proxy (via config reload) to switch traffic from the old container's port to the new one.
  • Then stopping the old container.

Database Migrations

If your application uses a database, schema migrations must be handled carefully. A good pattern is to run migrations before deploying the new application code. You can add a step in your deployment script that runs a migration command from a dedicated Docker image or script, ensuring the database is ready for the new code.

Pipeline Status Badges

Add a status badge to your project's README. GitHub Actions provides a markdown snippet that shows whether the main branch pipeline is passing or failing, signaling project health to collaborators and users.

Conclusion: From Side Project to Production-Ready Service

Building this automated CI/CD pipeline is an investment that pays continuous dividends. It transforms your side project from a fragile, manually deployed artifact into a robust service that can be updated with a simple git push. This automation reduces cognitive load, minimizes human error, and enforces code quality through automated testing.

You have now built a foundational DevOps skill set. The pattern of source control trigger → automated test/build → containerized deployment is universal. While we avoided Kubernetes for simplicity, the Docker images and practices you've implemented are perfectly compatible with it should your project's scale ever demand it. Start simple, deploy confidently, and iterate faster.