Install Jenkins on VPS for CI/CD Automation

Install Jenkins on VPS - CI/CD Automation Server

June 2026 · 9 min read

Jenkins is an open-source automation server used to build Continuous Integration and Continuous Deployment (CI/CD) pipelines — pull code from GitHub/GitLab automatically, run tests, compile, build, and deploy to production without manual intervention. This guide teaches you how to install Jenkins on a VPS using Docker, integrate it with GitHub, set up Declarative Pipelines using Jenkinsfile, and monitor builds visually with the Blue Ocean UI.

Why Use Jenkins on Your VPS

Install Jenkins with Docker

mkdir -p /opt/jenkins/{data,workspace} && cd /opt/jenkins

# Create docker-compose.yml
cat > docker-compose.yml << 'EOF'
version: "3.9"

services:
  jenkins:
    image: jenkins/jenkins:lts
    restart: unless-stopped
    ports:
      - "127.0.0.1:8080:8080"
      - "127.0.0.1:50000:50000"
    environment:
      JENKINS_OPTS: "-Djenkins.install.runSetupWizard=false"
      JENKINS_JAVA_OPTS: "-Xmx1024m -Xms512m"
    volumes:
      - ./data:/var/jenkins_home
      - ./workspace:/var/jenkins_home/workspace
      - /var/run/docker.sock:/var/run/docker.sock
    user: "0:0"

volumes:
  jenkins_data:
EOF

docker-compose up -d

# Wait 30 seconds and check logs
sleep 30
docker-compose logs -f jenkins | grep -i "jenkins is running"

Set Up Nginx Reverse Proxy for Jenkins

nano /etc/nginx/sites-available/jenkins

server {
    listen 80;
    server_name ci.yourdomain.com;
    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    server_name ci.yourdomain.com;

    ssl_certificate     /etc/letsencrypt/live/ci.yourdomain.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ci.yourdomain.com/privkey.pem;

    location / {
        proxy_pass http://127.0.0.1:8080;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto https;
        proxy_connect_timeout 600s;
        proxy_send_timeout 600s;
        proxy_read_timeout 600s;
    }
}

ln -s /etc/nginx/sites-available/jenkins /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx
certbot --nginx -d ci.yourdomain.com

Initialize Jenkins and Access the Web UI

# Get the initial admin password
docker-compose exec jenkins cat /var/jenkins_home/secrets/initialAdminPassword

# Visit https://ci.yourdomain.com
# Paste the password in the Setup Wizard
# Select "Install suggested plugins"
# Create your Admin User
# Save and Continue

# Configure Jenkins
# Manage Jenkins → Manage Plugins
# Search for "Blue Ocean" → Install
# Search for "GitHub Integration" → Install
# Restart Jenkins: curl -s http://127.0.0.1:8080/restart

Create a GitHub Personal Access Token

# GitHub → Settings → Developer settings → Personal access tokens → Tokens (classic)
# Generate new token
# Select scopes:
# ✓ repo (full control of private repositories)
# ✓ admin:repo_hook (write access to hooks)
# ✓ admin:org_hook (for org-level webhooks)
# Generate token → save it

# In Jenkins:
# Manage Jenkins → Credentials → System → Global credentials
# Add Credentials → Kind: GitHub App or Username with password
# Username: your-github-username
# Password: [paste the token]
# ID: github-credentials
# Save

Connect to GitHub and Create a Webhook

# Manage Jenkins → Configure System
# GitHub → GitHub Servers
# Add: https://api.github.com
# Credentials: [select github-credentials]
# Test connection → should show "Credentials verified"

# In your GitHub repository:
# Settings → Webhooks → Add webhook
# Payload URL: https://ci.yourdomain.com/github-webhook/
# Content type: application/json
# Events: Just the push event (or select individual events)
#   - ✓ Pushes
#   - ✓ Pull requests
# Active: ✓
# Add webhook

# Test the webhook
# In GitHub webhook: Recent Deliveries → check status is 200 OK

Create Your First Pipeline Job with Jenkinsfile

# In your GitHub repository, create Jenkinsfile at the root
cat > Jenkinsfile << 'EOF'
pipeline {
    agent any

    options {
        buildDiscarder(logRotator(numToKeepStr: '10'))
        timeout(time: 30, unit: 'MINUTES')
    }

    environment {
        NODE_ENV = 'production'
        REGISTRY = 'docker.io'
        IMAGE_NAME = 'myapp:${BUILD_NUMBER}'
    }

    stages {
        stage('Checkout') {
            steps {
                checkout scm
                sh 'git log -1 --oneline'
            }
        }

        stage('Install Dependencies') {
            steps {
                sh 'npm install'
            }
        }

        stage('Run Tests') {
            steps {
                sh 'npm test'
            }
        }

        stage('Build') {
            steps {
                sh 'npm run build'
            }
        }

        stage('Deploy') {
            when {
                branch 'main'
            }
            steps {
                sh '''
                    echo "Deploying to production..."
                    scp -r dist/ [email protected]:/var/www/app/
                    ssh [email protected] "cd /var/www/app && systemctl restart app"
                '''
            }
        }
    }

    post {
        always {
            cleanWs()
        }
        success {
            echo "Build succeeded!"
        }
        failure {
            echo "Build failed!"
        }
    }
}
EOF

# Commit to GitHub
git add Jenkinsfile
git commit -m "Add Jenkins pipeline"
git push origin main

# In Jenkins: New Item
# Item name: my-app-pipeline
# Type: Pipeline
# Pipeline → Definition: Pipeline script from SCM
# SCM: Git
# Repository URL: https://github.com/yourname/yourrepo.git
# Credentials: github-credentials
# Branch: */main
# Script Path: Jenkinsfile
# Save

# Test trigger: Click Build Now in Jenkins
# Or push code to GitHub — webhook will auto trigger

Set Up Blue Ocean Dashboard

# In Jenkins home → Open Blue Ocean
# Create a new pipeline
# Select GitHub
# Credentials: github-credentials
# Search your organization
# Select your repository
# Create pipeline

# Blue Ocean auto-scans Jenkinsfile and visualizes your pipeline
# Each stage appears as a card with status (Running/Success/Failed)
# Click a stage to view detailed logs

# Build history → view all past builds
# Activity → real-time monitor of current builds
Tip: Set up Jenkins agents on separate machines if your builds are resource-intensive — Jenkins master will dispatch jobs to agent nodes, preventing master from being blocked. Configure agents at: Manage Jenkins → Manage Nodes → New Node → Permanent Agent → Configure SSH key and labels

Verify Jenkins is Running Correctly

# Check container status
docker-compose ps

# Check Jenkins readiness
curl -s http://127.0.0.1:8080/api/json | python3 -m json.tool | head -20

# View Jenkins version
curl -s -I http://127.0.0.1:8080 | grep X-Jenkins

# List recent jobs
curl -s http://127.0.0.1:8080/api/json?tree=jobs[name,lastBuild[number,result]] \
  | python3 -m json.tool

# Check available executors
curl -s http://127.0.0.1:8080/api/json?tree=numExecutors | python3 -m json.tool

# Verify GitHub webhook delivery
# Repository → Settings → Webhooks → [webhook URL] → Recent Deliveries
Tip: If Jenkins jobs don't auto-trigger from webhooks, verify: (1) webhook URL is accessible from outside your network (not localhost) (2) firewall allows GitHub IPs to connect (3) check Jenkins logs: docker-compose logs jenkins | grep -i webhook

Troubleshoot Common Errors

Error: "Failed to connect to GitHub"

Jenkins cannot reach GitHub API or credentials are incorrect.

# Check Jenkins credentials
# Manage Jenkins → Credentials → System → Global credentials
# Verify github-credentials exists

# Test GitHub token directly
curl -H "Authorization: token YOUR_GITHUB_TOKEN" \
  https://api.github.com/user

# If token expired, regenerate in GitHub → Settings → Developer settings
# Update credentials in Jenkins

# Test DNS inside container
docker-compose exec jenkins nslookup api.github.com

# If DNS fails, add extra hosts in docker-compose
# services:
#   jenkins:
#     extra_hosts:
#       - "api.github.com:140.82.113.6"

Error: "Webhook delivery failed with status 403"

GitHub webhook sent the payload but Jenkins rejected it (Forbidden) — usually due to permissions or IP blocking.

# Check Jenkins firewall
sudo ufw status
# Ensure port 8080 or 443 (via nginx proxy) is open

# Verify nginx config for /github-webhook endpoint
# Must have location rule:
# location /github-webhook/ {
#     proxy_pass http://127.0.0.1:8080/github-webhook/;
# }

# Temporarily disable CSRF protection for testing (not recommended for production):
# Manage Jenkins → Configure Global Security
# CSRF Protection: uncheck [only for testing!]

# View Jenkins logs
docker-compose logs jenkins | tail -50

Error: "Build failed: No executor available"

Jenkins master node has no free executors (all busy) or the build is queued.

# Check executor count
curl -s http://127.0.0.1:8080/api/json?tree=numExecutors

# Check the queue
curl -s http://127.0.0.1:8080/api/json?tree=queue[items[id,task[name],why]] \
  | python3 -m json.tool

# Increase executor count:
# Manage Jenkins → Configure System → # of executors → increase to 4 or more
# Save

# Check for Java memory issues
docker-compose exec jenkins free -h
docker-compose logs jenkins | grep -i "OutOfMemory"

# If OutOfMemory, increase memory in docker-compose:
# environment:
#   JENKINS_JAVA_OPTS: "-Xmx2048m -Xms1024m"

Error: "Pipeline syntax error in Jenkinsfile"

Your Jenkinsfile has syntax errors — incorrect stages or indentation.

# Validate syntax with Jenkins Pipeline Syntax page
# Jenkins → Pipeline Syntax → Declarative Directive Generator
# Paste Jenkinsfile and validate

# Common mistakes:
# - stages must have at least 1 stage block
# - steps must contain sh / bat / echo etc.
# - when blocks need conditions like branch() / expression()
# - post blocks need {always/success/failure} etc.

# Validate Jenkinsfile with GitHub
# Settings → Code security & analysis → Dependabot alerts

# Check for shebangs
cat Jenkinsfile | head -1
# Must NOT start with #! — Jenkins handles it

Error: "ssh deploy@server: Permission denied (publickey)"

Jenkins container doesn't have SSH key to deploy to your production server.

# Generate SSH key in Jenkins container
docker-compose exec jenkins ssh-keygen -t ed25519 -f ~/.ssh/id_jenkins -N ""

# View the public key
docker-compose exec jenkins cat ~/.ssh/id_jenkins.pub

# Add it to your destination server:
# ~/.ssh/authorized_keys of the deploy user

# Test SSH connection
docker-compose exec jenkins ssh -i ~/.ssh/id_jenkins [email protected] "whoami"

# If still failing, configure known_hosts:
docker-compose exec jenkins ssh-keyscan -t ed25519 yourdomain.com >> ~/.ssh/known_hosts

Frequently Asked Questions

Jenkins vs GitHub Actions - which is better?

GitHub Actions is free for 3000 minutes/month and simpler for public repos. Jenkins on VPS requires hosting costs but offers unlimited builds and full control. For private repos with heavy workloads (100+ build hours/month), Jenkins is more economical than GitHub Actions free tier. For enterprise use, Jenkins provides more flexibility: custom plugins, agent distribution across servers, centralized artifact management, and integration with on-premises infrastructure.

Is it safe to use GitHub Personal Access Token in Jenkins?

Yes, it's safe if you store the token in Jenkins Credentials (Jenkins encrypts it automatically) and limit the token's scope (repo + webhook permissions only, no delete repo access). Even more secure is using a GitHub App instead of PAT — GitHub Apps have expirable tokens and separate rate limits. See the Jenkins GitHub App plugin for setup instructions.

What's the difference between Jenkins master and agent nodes?

Master (controller) manages the UI, credentials, and job scheduling. If master has free executors, it can run builds directly. Agents are separate machines that receive jobs from master — ideal for heavy builds like Docker image builds or GPU-intensive tests. For scalability: add more agent nodes, and master distributes jobs automatically. Production Jenkins setups typically use 1 master + 5-10 agent nodes.