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:
- Source Control: GitHub repository with branch protection
- CI Runner: Self-hosted GitHub Actions runner on your VPS
- Build: Docker image build and push to container registry
- Deploy: Blue-green deployment from Docker Compose via SSH
- Verify: Health check endpoint validation after deployment
- Notify: Slack/Discord webhook on deployment status
Prerequisites
Before you begin, you'll need:
- A UK VPS from Hostingowy (or any Linux VPS with root access)
- Ubuntu 22.04 or 24.04 LTS installed
- A GitHub repository for your application
- Basic familiarity with Docker and command line
- Domain name pointed to your VPS (optional but recommended)
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
- Use GitHub Secrets โ Never hardcode API keys, database URLs, or tokens in your workflow files
- Restrict runner access โ Limit the self-hosted runner user to only necessary directories and Docker access
- Enable branch protection โ Require pull request reviews and passing checks before merging to main
- Rotate tokens regularly โ Regenerate your runner registration token periodically
- Use read-only deployments โ The deploy user should only have write access to the application directory
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