CI/CD Pipeline on VPS: Complete Setup Guide 2025

How to set up a production-grade CI/CD pipeline on your UK VPS using self-hosted runners, Docker, and automated zero-downtime deployments.

A well-configured CI/CD pipeline is the backbone of modern software delivery. When you're running applications on a UK VPS, setting up automated deployments saves hours of manual work and eliminates deployment errors.

In this guide, we'll walk through setting up a complete CI/CD pipeline on your VPS using GitHub Actions with self-hosted runners, Docker containers, and Nginx reverse proxy for zero-downtime deployments. This exact setup powers our own infrastructure at Hostingowy.

Architecture Overview

Our CI/CD pipeline follows a clean, layered architecture:

Prerequisites

Before you begin, you'll need:

Step 1: Initial VPS Setup

Start with a fresh VPS. We recommend following our Linux server hardening guide for security best practices, then install Docker:

# Update system packages
sudo apt update && sudo apt upgrade -y

# Install Docker
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

# Install Docker Compose v2
sudo apt install -y docker-compose-plugin

# Add your user to docker group
sudo usermod -aG docker $USER

# Log out and back in, or run:
newgrp docker

# Verify installation
docker --version && docker compose version

Step 2: Set Up the Application Directory Structure

# Create application directory
sudo mkdir -p /opt/applications/your-app
sudo chown -R $USER:$USER /opt/applications

# Create deployment structure
mkdir -p /opt/applications/your-app/{src,nginx,data,backups}

Step 3: Configure Docker Compose for Production

Create a production-ready docker-compose.yml in your application directory:

version: '3.8'

services:
  app:
    image: ghcr.io/yourorg/your-app:latest
    restart: unless-stopped
    expose:
      - "3000"
    environment:
      - NODE_ENV=production
      - DATABASE_URL=${DATABASE_URL}
      - REDIS_URL=${REDIS_URL}
    volumes:
      - ./data:/app/data
    networks:
      - app-network
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  nginx:
    image: nginx:alpine
    restart: unless-stopped
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./nginx:/etc/nginx/conf.d
      - ./data/ssl:/etc/nginx/ssl
    networks:
      - app-network
    depends_on:
      app:
        condition: service_healthy

networks:
  app-network:
    driver: bridge

Step 4: Install and Configure the Self-Hosted Runner

Install a GitHub Actions self-hosted runner on your VPS for direct deployment:

# Create a dedicated user for the runner
sudo useradd -m -s /bin/bash github-runner
sudo -u github-runner mkdir -p /home/github-runner/actions-runner
cd /home/github-runner/actions-runner

# Download the latest runner (check GitHub for current version)
curl -o actions-runner-linux-x64-2.316.1.tar.gz \
  -L https://github.com/actions/runner/releases/download/v2.316.1/actions-runner-linux-x64-2.316.1.tar.gz
tar xzf actions-runner-linux-x64-2.316.1.tar.gz

# Configure the runner
# Get your token from: GitHub repo โ†’ Settings โ†’ Actions โ†’ Runners โ†’ New self-hosted runner
sudo -u github-runner ./config.sh --url https://github.com/yourorg/your-app \
  --token YOUR_TOKEN_HERE --labels vps-production

# Install as a service
sudo ./svc.sh install github-runner
sudo ./svc.sh start

Step 5: Create the CI/CD Workflow

Create .github/workflows/deploy.yml in your repository:

name: Deploy to Production

on:
  push:
    branches: [main]
  workflow_dispatch:

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Run tests
        run: |
          npm ci
          npm test

  build-and-push:
    needs: test
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: actions/checkout@v4
      - name: Log in to Container Registry
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

  deploy:
    needs: build-and-push
    runs-on: [self-hosted, vps-production]
    steps:
      - name: Deploy via Docker Compose
        run: |
          cd /opt/applications/your-app
          docker compose pull app
          docker compose up -d --force-recreate app
          echo "Waiting for health check..."
          sleep 15
          docker compose ps
      - name: Verify deployment
        run: |
          curl -f http://localhost:3000/health || exit 1
          echo "โœ… Deployment verified successfully"

Step 6: Zero-Downtime Deployments with Blue-Green

For production systems, use a blue-green deployment pattern to eliminate downtime:

# docker-compose.bluegreen.yml
version: '3.8'

services:
  app-blue:
    image: ghcr.io/yourorg/your-app:latest
    expose: ["3000"]
    networks:
      - app-network

  app-green:
    image: ghcr.io/yourorg/your-app:latest  
    expose: ["3001"]
    networks:
      - app-network

Your Nginx configuration would then switch traffic between blue and green based on which version is healthy. This ensures zero-downtime deployments โ€” critical for production applications.

Step 7: Monitoring and Alerting

Add deployment notifications to your pipeline for visibility:

# Add to the deploy job steps
- name: Notify on Success
  if: success()
  run: |
    curl -X POST -H "Content-Type: application/json" \
      -d '{"text":"โœ… Deployed main to production successfully"}' \
      ${{ secrets.SLACK_WEBHOOK_URL }}

- name: Notify on Failure
  if: failure()
  run: |
    curl -X POST -H "Content-Type: application/json" \
      -d '{"text":"โŒ Deployment to production failed!"}' \
      ${{ secrets.SLACK_WEBHOOK_URL }}

Security Best Practices

Common Pitfalls and Solutions

Problem: Runner loses connectivity

Configure the runner service to restart automatically: sudo ./svc.sh restart and add monitoring with our Grafana + Prometheus guide.

Problem: Disk space filling up from old Docker images

Add a cleanup job to your workflow: docker system prune -af --filter "until=24h" to remove unused images and containers.

Problem: Long-running deployments timing out

Increase the GitHub Actions job timeout: timeout-minutes: 30 at the job level, and consider using a build cache.

Conclusion

A CI/CD pipeline on your VPS transforms how you ship software. With self-hosted runners and Docker Compose, you get the same deployment automation that large cloud providers offer โ€” at a fraction of the cost. Our own infrastructure at Hostingowy runs on this exact architecture, deployed across European data centres (Poland).

Ready to Build Your CI/CD Pipeline?

Get a UK VPS with KVM + NVMe starting at $5/mo. First month free with LAUNCH100.

Deploy Your VPS โ†’

Related guides: VPS Hosting Complete Guide โ€ข Linux Server Hardening Guide โ€ข Infrastructure Monitoring with Grafana & Prometheus

๐Ÿš€

The Hostingowy Engineering Digest

Get monthly deep-dives on UK VPS hosting, infrastructure benchmarks, DevOps tooling, and cloud cost optimisation. Built by engineers, for engineers.

2 issues sent ยท 7 subscribers ยท No spam, ever

No spam. Unsubscribe anytime. Read our Privacy Policy.