GitHub Actions Self-Hosted Runner Pipeline Setup

By David Bakare
August 29, 2025
5 min read
GitHub ActionsCI/CDDevOpsDockerLinux
Github Actions

Github Actions

This guide provides a comprehensive, step-by-step walkthrough to set up a CI/CD pipeline using a GitHub Actions self-hosted runner on a Virtual Private Server (VPS). This approach is highly flexible and cost-effective, allowing for full control over your build and deployment environment.

Setup Steps

1. SSH into the VPS

ssh user@VPS_IP

2. System Update

sudo apt update && sudo apt upgrade -y

3. Install Docker and Docker Compose

# Install Docker
curl -fsSL [https://get.docker.com](https://get.docker.com) | sh

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

# Log out and back in for group changes to take effect
# Or use: newgrp docker

# Verify Docker installation
docker --version

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

# Verify Docker Compose installation
docker compose version

4. Configure GitHub Actions Runner

Get Runner Token

  1. Navigate to your GitHub repository
  2. Go to Settings → Actions → Runners
  3. Click "New self-hosted runner"
  4. Copy the registration token

Install Runner

# Create runner directory
mkdir ~/actions-runner && cd ~/actions-runner

# Download latest runner package
curl -o actions-runner-linux-x64.tar.gz -L \
  [https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64.tar.gz](https://github.com/actions/runner/releases/latest/download/actions-runner-linux-x64.tar.gz)

# Extract the archive
tar xzf ./actions-runner-linux-x64.tar.gz

# Configure the runner (replace with your values)
./config.sh --url [https://github.com/USERNAME/REPO](https://github.com/USERNAME/REPO) --token YOUR_TOKEN

Test Runner

# Test manually
./run.sh

# Once confirmed working, stop with Ctrl+C

Install as Service

# Install as systemd service
sudo ./svc.sh install

# Start the service
sudo ./svc.sh start

# Check service status
sudo ./svc.sh status

5. Project Setup on VPS

Create project directory structure:

mkdir ~/your-project
cd ~/your-project

Configuration Files

Docker Compose Configuration

Create docker-compose.yml:

version: "3.8"

services:
  app:
    image: your-dockerhub-username/your-app:latest
    container_name: your-app
    restart: always
    ports:
      - "80:80"
    environment:
      - NODE_ENV=production
    # Optional: Add volumes for persistent data
    # volumes:
    #   - ./data:/app/data

Dockerfile Example

Multi-stage Dockerfile for Node.js application:

# Stage 1: Build
FROM node:20-alpine AS builder

WORKDIR /app

# Enable pnpm (if using pnpm)
RUN corepack enable pnpm

# Copy dependency files
COPY package.json pnpm-lock.yaml ./

# Install dependencies
RUN pnpm install --frozen-lockfile

# Copy source code
COPY . .

# Build application
RUN pnpm run build

# Stage 2: Production
FROM nginx:alpine AS runner

# Remove default nginx static files
RUN rm -rf /usr/share/nginx/html/*

# Copy built application from builder stage
COPY --from=builder /app/dist /usr/share/nginx/html

# Copy nginx configuration if needed
# COPY nginx.conf /etc/nginx/conf.d/default.conf

EXPOSE 80

CMD ["nginx", "-g", "daemon off;"]

GitHub Actions Workflow

Create .github/workflows/deploy.yml:

name: CI/CD Pipeline

on:
  push:
    branches:
      - main
  pull_request:
    branches:
      - main

env:
  DOCKER_IMAGE: your-dockerhub-username/your-app

jobs:
  build-and-deploy:
    runs-on: self-hosted
    
    steps:
      - name: Checkout code
        uses: actions/checkout@v4

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Log in to Docker Hub
        uses: docker/login-action@v3
        with:
          username: ${{ secrets.DOCKER_USERNAME }}
          password: ${{ secrets.DOCKER_PASSWORD }}

      - name: Build and push Docker image
        uses: docker/build-push-action@v5
        with:
          context: .
          push: true
          tags: |
            ${{ env.DOCKER_IMAGE }}:latest
            ${{ env.DOCKER_IMAGE }}:${{ github.sha }}
          cache-from: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache
          cache-to: type=registry,ref=${{ env.DOCKER_IMAGE }}:buildcache,mode=max

      - name: Deploy with Docker Compose
        run: |
          cd ~/your-project
          docker compose pull
          docker compose up -d --force-recreate
          docker system prune -f

      - name: Health check
        run: |
          sleep 10
          curl -f http://localhost || exit 1

GitHub Secrets Configuration

Add these secrets to your GitHub repository (Settings → Secrets and variables → Actions):

  • DOCKER_USERNAME: Your Docker Hub username
  • DOCKER_PASSWORD: Your Docker Hub password or access token

Nginx Configuration (Optional)

If using Nginx, create nginx.conf:

server {
    listen 80;
    server_name _;
    
    root /usr/share/nginx/html;
    index index.html;
    
    # Enable gzip compression
    gzip on;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
    
    location / {
        try_files $uri $uri/ /index.html;
    }
    
    # Cache static assets
    location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
        expires 1y;
        add_header Cache-Control "public, immutable";
    }
}

Maintenance Commands

Runner Management

# Check runner status
sudo ./svc.sh status

# Stop runner
sudo ./svc.sh stop

# Start runner
sudo ./svc.sh start

# Uninstall runner service
sudo ./svc.sh uninstall

Docker Cleanup

# Remove unused containers, networks, images
docker system prune -a -f

# Check disk usage
docker system df

# View logs
docker compose logs -f

# Restart services
docker compose restart

Troubleshooting

Common Issues and Solutions

  1. Runner not starting

    # Check logs
    journalctl -u actions.runner.* -f
    
  2. Permission denied errors

    # Ensure user is in docker group
    groups $USER
    # Re-login if needed
    
  3. Port already in use

    # Find process using port
    sudo lsof -i :80
    # Stop conflicting service or change port
    
  4. Docker image pull failures

    # Verify Docker Hub credentials
    docker login
    # Check image name and tag
    

Security Best Practices

  1. Use secrets for sensitive data - Never hardcode credentials
  2. Limit runner permissions - Use dedicated user for runner
  3. Regular updates - Keep system and dependencies updated
  4. Use HTTPS - Configure SSL/TLS with Let's Encrypt
  5. Firewall configuration - Only open necessary ports
  6. Monitor logs - Set up log aggregation and monitoring

Additional Resources

Notes

  • This setup is project-agnostic and can be adapted for any application
  • Adjust image names, ports, and environment variables as needed
  • Consider using environment-specific configurations for staging/production
  • The runner automatically starts on system boot via systemd service