VPS • Git • CI/CD • Client Websites

Setting Up a Git-Based CI/CD Deployment Pipeline on a VPS for Client Sites

Stop uploading files manually, logging into servers for every release, and wondering which version is actually live. A Git-based CI/CD pipeline can turn a VPS into a predictable deployment system where every approved code change follows the same build, test, deploy, and verification process.

This guide shows how agencies and freelance developers can build that workflow for client websites using GitHub Actions, SSH, a Linux VPS, Nginx, and a simple release structure that makes rollbacks much easier.

Recommended deployment flow
Git Push → CI Tests → Build → VPS → Health Check

The goal is not simply faster deployment. The goal is repeatable deployment.

Quick answer: A practical VPS CI/CD pipeline uses Git as the source of truth, a CI service such as GitHub Actions or GitLab CI/CD for automated testing, an SSH-based deployment credential with limited permissions, a predictable release directory on the VPS, and a post-deployment health check. For client work, add staging, protected production environments, backups, deployment logs, and a rollback process before enabling automatic production releases.

Why CI/CD Matters When You Manage Multiple Client Sites

The first few client websites rarely feel difficult to deploy. You SSH into the VPS, pull the latest code, clear a cache, restart a service, and move on.

Then the agency grows.

Five websites become fifteen. Fifteen become thirty. Different clients have different release schedules. One project uses PHP, another uses Node.js, another has a build process, and another depends on Docker. Suddenly, deployment is no longer a technical afterthought. It becomes an operational risk.

That is where Git-based CI/CD becomes valuable.

Consistency

Every deployment follows the same defined process instead of relying on someone's memory.

Traceability

You can identify which commit was deployed and connect a production change to a specific Git revision.

Rollback

A release-based structure lets you return to a known working version without manually reconstructing the previous deployment.

For agencies, the biggest benefit is often not saving a few minutes on one deployment. It is reducing the number of small decisions humans have to make during every deployment.

If you are still deciding between shared, VPS, and cloud infrastructure, start with this guide to shared vs VPS vs cloud hosting for small businesses.

The Recommended Git-Based CI/CD Architecture

A reliable deployment pipeline should have clear responsibilities. Git stores the desired application state. CI validates that state. The VPS runs the approved release. Monitoring confirms that the release actually works.

1. Git repository

Contains application code, configuration templates, deployment scripts, and workflow definitions.

2. CI system

Runs tests, linting, builds, and other automated checks before production deployment.

3. VPS

Hosts the production application and receives approved releases through a controlled deployment account.

Layer Primary responsibility Example
Source control Version history and collaboration GitHub or GitLab
CI Test and build the application GitHub Actions or GitLab CI/CD
Transport Move approved release to server SSH
Runtime Serve the application Nginx + PHP-FPM / Node / Docker
Verification Confirm the site responds correctly HTTP health check

GitHub Actions supports deployment environments that can restrict branches, require approvals, and control access to environment secrets. This makes it possible to keep a production deployment different from a normal test workflow.

GitLab follows a similar model with environments such as staging and production, while also tracking deployments and allowing protected environment variables.

You can review GitLab's environment model in its official CI/CD environments documentation.

What You Need Before Starting

This tutorial assumes a Linux VPS, a Git repository, and an application that can be deployed from source control.

A Linux VPS with SSH access
A domain pointed to the VPS
Git installed on the server
Nginx or another web server
A private Git repository
A dedicated deployment user
SSH authentication
A staging environment if possible

For an agency, I strongly recommend keeping production and staging separate even if both run on the same VPS initially. The environments should have different paths, databases, environment variables, and deployment controls.

Important: Do not make the CI runner a root-level deployment mechanism simply because it is easier. A compromised workflow should not automatically receive unrestricted control over every client site on the server.

Step 1: Prepare the VPS for Automated Deployment

Start with a normal Linux server rather than trying to make the deployment pipeline responsible for server administration.

A good VPS baseline includes a current operating system, firewall rules, SSH hardening, a non-root administrative user, automated security updates where appropriate, backups, monitoring, and a web server configured separately from the application release process.

On Ubuntu Server, security updates can be handled through the unattended-upgrades system. Review your reboot and package policies carefully before enabling automatic changes on a production server.

If the VPS is located in a US-East, US-West, London, or Frankfurt region, the same deployment architecture can be used. The important issue is consistency between your client requirements, application users, database location, and operational policies.

Suggested directory layout

/var/www/client-example/
├── current -> releases/20260926001
├── releases/
│   ├── 20260926001/
│   ├── 20260926002/
│   └── 20260926003/
├── shared/
│   ├── .env
│   ├── uploads/
│   └── storage/
└── deploy/
    └── deploy.sh

The important idea is the current symlink. Nginx points to the current release rather than to a directory that is overwritten file by file.

That small architectural decision makes rollbacks considerably cleaner.

For VPS planning, it is also useful to understand the differences between managed and unmanaged cloud infrastructure. Our guide to managed cloud hosting for US small businesses provides useful context.

Step 2: Structure the Git Repository for Deployment

A CI/CD pipeline becomes much easier to maintain when the repository clearly separates application code from production-only configuration.

A typical project might look like this:

client-site/
├── .github/
│   └── workflows/
│       └── deploy.yml
├── app/
├── public/
├── tests/
├── scripts/
│   ├── build.sh
│   └── deploy.sh
├── composer.json
├── package.json
├── package-lock.json
├── .gitignore
└── README.md

Never place production passwords, API tokens, database credentials, private SSH keys, or client secrets directly inside the repository.

GitHub Actions provides repository and environment secrets for sensitive values. Environment secrets can also be protected by required reviewers, depending on how the production environment is configured.

Read the GitHub Actions secrets documentation before designing your credential model.

Step 3: Create a Dedicated Deployment User

The deployment process should have its own Linux identity.

For example:

sudo adduser deploy
sudo usermod -aG www-data deploy

The exact permissions depend on your application. Do not blindly copy the example above into a sensitive production system. Your goal is to give the deployment user enough access to publish the application, but not unnecessary access to unrelated client data or operating-system administration.

Generate an SSH key for deployment

On your administrative machine, create a dedicated key:

ssh-keygen -t ed25519 -C "client-site-deployment"

Add the public key to the deployment user's authorized_keys file on the VPS. The private key should remain outside the repository and should be stored as a protected CI secret.

Do not reuse your personal SSH key. A deployment credential should be revocable independently from your personal administrative access.

Step 4: Build the GitHub Actions CI/CD Workflow

GitHub Actions workflow files live inside .github/workflows. A good deployment pipeline should not jump directly from a developer's laptop to production.

A safer flow is:

Pull Request → Test → Build → Merge → Deploy → Verify

Example GitHub Actions workflow

The following example is intentionally conservative. It tests the application before deployment, limits repository token permissions, uses a production environment, prevents overlapping production deployments, and performs the server deployment over SSH.

name: Deploy Client Site

on:
  push:
    branches:
      - main
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: production-deployment
  cancel-in-progress: false

jobs:

  test:
    name: Test application
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v5

      - name: Install dependencies
        run: |
          if [ -f composer.json ]; then
            composer install --no-interaction --prefer-dist
          fi

          if [ -f package-lock.json ]; then
            npm ci
          fi

      - name: Run tests
        run: |
          if [ -f package.json ]; then
            npm test --if-present
          fi

  deploy:
    name: Deploy to production
    needs: test
    runs-on: ubuntu-latest
    environment: production

    steps:
      - name: Checkout repository
        uses: actions/checkout@v5

      - name: Configure SSH
        env:
          DEPLOY_KEY: ${{ secrets.DEPLOY_KEY }}
          VPS_HOST: ${{ secrets.VPS_HOST }}
        run: |
          mkdir -p ~/.ssh
          chmod 700 ~/.ssh

          printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/deploy_key
          chmod 600 ~/.ssh/deploy_key

          ssh-keyscan -H "$VPS_HOST" >> ~/.ssh/known_hosts

      - name: Create deployment archive
        run: |
          tar \
            --exclude=".git" \
            --exclude=".github" \
            --exclude="node_modules" \
            -czf release.tar.gz .

      - name: Upload release
        env:
          VPS_HOST: ${{ secrets.VPS_HOST }}
          VPS_USER: ${{ secrets.VPS_USER }}
        run: |
          scp \
            -i ~/.ssh/deploy_key \
            release.tar.gz \
            "$VPS_USER@$VPS_HOST:/tmp/client-site-release.tar.gz"

      - name: Deploy release
        env:
          VPS_HOST: ${{ secrets.VPS_HOST }}
          VPS_USER: ${{ secrets.VPS_USER }}
        run: |
          ssh \
            -i ~/.ssh/deploy_key \
            "$VPS_USER@$VPS_HOST" \
            "/var/www/client-example/deploy/deploy.sh"

      - name: Verify website
        env:
          SITE_URL: ${{ secrets.SITE_URL }}
        run: |
          curl \
            --fail \
            --silent \
            --show-error \
            --location \
            --max-time 20 \
            "$SITE_URL"

The exact action versions, language setup, dependency installation, and test commands should be adjusted to your application. The important pattern is the separation between testing, deployment, and verification.

GitHub's current workflow syntax supports explicitly limiting the permissions granted to the workflow token. Using the smallest permission set needed is a useful baseline for reducing unnecessary workflow access.

Why concurrency matters

Imagine two developers merge changes within thirty seconds of each other.

Without deployment coordination, both releases could attempt to modify the same production target simultaneously.

A concurrency group can ensure that only one production deployment runs at a time. GitHub Actions supports concurrency controls specifically for this kind of workflow coordination.

Step 5: Create the VPS Deployment Script

The CI workflow should not contain every server-side deployment decision. Put the VPS-specific operations into a versioned deployment script.

Example:

#!/usr/bin/env bash

set -Eeuo pipefail

APP_ROOT="/var/www/client-example"
RELEASES="$APP_ROOT/releases"
CURRENT="$APP_ROOT/current"

TIMESTAMP="$(date +%Y%m%d%H%M%S)"
NEW_RELEASE="$RELEASES/$TIMESTAMP"

mkdir -p "$NEW_RELEASE"

tar \
  -xzf /tmp/client-site-release.tar.gz \
  -C "$NEW_RELEASE"

rm -f /tmp/client-site-release.tar.gz

# Install or build production dependencies here.
# Examples:
# cd "$NEW_RELEASE"
# composer install --no-dev --optimize-autoloader
# npm ci
# npm run build

ln -sfn "$NEW_RELEASE" "$CURRENT"

# Reload application services only when required.
# sudo systemctl reload php8.3-fpm

# Keep the latest five releases.
cd "$RELEASES"

ls -1dt */ 2>/dev/null \
  | tail -n +6 \
  | xargs -r rm -rf

echo "Deployment completed: $TIMESTAMP"

Make the script executable:

chmod 750 /var/www/client-example/deploy/deploy.sh

The release directory gives every deployment its own filesystem state. The current symlink is then switched to the new release.

Why this matters: If release 20260926001 works and release 20260926002 introduces a problem, you do not have to guess which files changed. The previous release still exists.

Point Nginx at the Current Release

Your web server should serve the stable current path rather than an individual release directory.

server {
    listen 80;
    server_name client-example.com www.client-example.com;

    root /var/www/client-example/current/public;
    index index.php index.html;

    location / {
        try_files $uri $uri/ /index.php?$query_string;
    }

    location ~ \.php$ {
        include snippets/fastcgi-php.conf;
        fastcgi_pass unix:/run/php/php8.3-fpm.sock;
    }
}

If you are deploying a Node.js application instead, Nginx can act as a reverse proxy in front of the application process. The exact configuration depends on the runtime and process manager.

Nginx documents reverse proxying through the proxy_pass directive, which forwards requests from the web server to the upstream application.

Step 6: Build Rollback Into the Pipeline Before You Need It

A deployment system is incomplete if it only knows how to move forward.

A client may report that a release broke a form, payment flow, login process, CSS asset, API connection, or checkout page. Your first objective should be restoring service safely. Investigation can come afterward.

Simple rollback

cd /var/www/client-example/releases

ls -1dt */

ln -sfn \
  /var/www/client-example/releases/20260926001 \
  /var/www/client-example/current

In a mature agency workflow, you can automate rollback further by storing the active release ID, running health checks after deployment, and triggering a rollback when a critical check fails.

Fast rollback

Keep several known-good releases available on the VPS.

Database safety

Treat database migrations separately because application files can be rolled back more easily than destructive schema changes.

Client communication

Maintain a deployment log so the team can identify what changed and when.

How to Adapt CI/CD for WordPress Client Sites

WordPress requires a slightly different deployment strategy because the website contains both code and user-generated content.

Theme files, custom plugins, configuration templates, and application code can fit naturally into Git. The uploads directory and production database generally should not be treated as ordinary Git-controlled source files.

WordPress component Typical approach Why
Custom theme Git Version-controlled source code
Custom plugin Git Repeatable releases
Uploads Persistent shared storage Contains client-generated media
Database Backup + migration process Changes continuously in production
wp-config.php Environment-specific configuration Contains sensitive values
Cache Explicit invalidation Prevents old assets/pages remaining active

This is particularly important for WooCommerce. Orders, customers, sessions, inventory, and payment-related activity can change while a deployment is taking place.

If performance is part of your deployment strategy, our WooCommerce hosting and TTFB comparison provides useful context around production performance testing.

Likewise, if the client site handles regulated information, review your deployment, access, backup, and hosting architecture against the applicable compliance requirements rather than treating CI/CD alone as a compliance control.

For example, our guide to HIPAA-compliant web hosting explains why infrastructure, contracts, access controls, and operational procedures matter together.

Manual Deployment vs Git-Based CI/CD

The difference becomes clearer when you look at a normal agency workflow rather than a theoretical software-development diagram.

Deployment activity Manual VPS process Git-based CI/CD Operational effect
Code selection Developer decides what to upload Git commit defines release Clear version history
Testing Often manual Automated checks Problems can be caught earlier
File transfer FTP/SFTP/SSH Automated deployment job Less repetitive work
Server commands Typed manually Versioned script More consistent releases
Rollback Restore backup or manually reverse files Switch to previous release Faster recovery path
Deployment history May exist in chat or memory Stored in Git and CI system Better accountability
Production approval Human discretion Environment protection rules More controlled release process

A Real-World Agency Workload Model

Consider a small web agency managing 20 client websites. Assume the team deploys each site twice per month and a manual deployment takes approximately 15 minutes of hands-on work.

That produces:

20 Client sites
40 Deployments/month
10 hrs Manual deployment work
15 min Example manual deployment

If automation reduces hands-on deployment work to roughly four minutes per release because the developer mainly reviews the result and handles exceptions, the same workload becomes about 2 hours and 40 minutes of hands-on deployment activity.

Agency size Sites Deployments/site/month Total releases/month Manual time at 15 min
Small 10 2 20 5 hours
Growing 20 2 40 10 hours
Established 50 2 100 25 hours
High-change portfolio 50 4 200 50 hours
Important: These figures are an agency planning model, not a controlled benchmark. Actual deployment time varies widely depending on application complexity, tests, database migrations, cache invalidation, approval processes, and server architecture.

The larger point is that automation becomes increasingly valuable as deployment frequency and client count rise.

You can also compare this operational approach with broader cloud hosting models in our Cloudways vs Hostinger cloud performance analysis.

GitHub Actions vs GitLab CI/CD for VPS Deployment

Both platforms can handle the core workflow. The best choice usually depends on where your repositories already live and how your team prefers to manage environments.

Capability GitHub Actions GitLab CI/CD
Pipeline definition YAML workflow files .gitlab-ci.yml
Production environments Supported Supported
Environment secrets Supported Supported through CI/CD variables and scopes
Deployment history Available through deployment/environment features Strong environment and deployment tracking
Manual approval Environment protection rules Manual deployment jobs and protected environments
Self-hosted execution Self-hosted runners GitLab runners
Best starting point Teams already using GitHub Teams already using GitLab

There is no need to move repositories simply to implement CI/CD. If the agency already works in GitHub, GitHub Actions is a natural starting point. If the development team already uses GitLab, GitLab CI/CD provides the equivalent deployment concepts.

CI/CD Security: The Part Agencies Should Not Rush

Automation increases capability. That also means a compromised workflow can potentially become a deployment pathway into production.

Treat your CI pipeline as production infrastructure.

Security control Recommended approach
SSH key Dedicated deployment key with limited scope
Linux account Dedicated deploy user rather than root
GitHub token Minimum required permissions
Production secrets Protected environment secrets
Repository Private for sensitive client applications
Workflow dependencies Pin and review third-party actions carefully
Production deploy Require review for sensitive projects
Logs Never print passwords, private keys, or tokens
Backups Maintain independent recovery copies

GitHub recommends least-privilege permissions for workflow tokens and provides protected environment secrets. Environment protection can require approval before a deployment job receives access to production secrets.

Never put this in Git: database passwords, API keys, cloud credentials, private SSH keys, payment credentials, customer secrets, production environment files, or backup encryption keys.

If your VPS hosts several unrelated clients, consider stronger isolation as the portfolio grows. A single server compromise should not automatically expose every client application.

Step 7: Add Post-Deployment Health Checks

A deployment can complete successfully while the website is still broken.

The SSH command might exit with status zero. The files might exist. Nginx might still be running. Yet the application could return HTTP 500, fail to connect to the database, load broken assets, or expose an error page.

That is why every production pipeline should include at least one application-level health check.

curl \
  --fail \
  --silent \
  --show-error \
  --location \
  --max-time 20 \
  https://client-example.com/health

For a more advanced application, create a dedicated health endpoint that verifies the components required for the application to serve normal traffic.

HTTP status

Confirm the site returns an expected HTTP response.

Application health

Check that the application process and dependencies respond correctly.

Critical path

For ecommerce sites, test important pages such as product and checkout flows without exposing customer data.

Do Not Forget Cache Invalidation

A successful deployment does not necessarily mean visitors immediately receive the new version.

WordPress sites can have several caching layers:

Layer Example Deployment consideration
Browser User browser cache Use versioned assets
WordPress plugin Page/object cache Clear or warm appropriately
Server FastCGI/object cache Review after application changes
CDN Edge cache Invalidate only what changed where possible
PHP opcode cache OPcache Reload PHP-FPM when required by the deployment

Cache invalidation should be a deliberate pipeline step, not a random command someone remembers after the deployment.

For agencies working with high-traffic sites, also review Cloudflare Enterprise vs Fastly edge caching and DDoS considerations when designing the broader delivery architecture.

10 Common CI/CD Mistakes on VPS Hosting

  1. Deploying directly as root. A deployment account should not automatically have unrestricted server access.
  2. Keeping secrets in Git. Use protected CI/CD secrets instead.
  3. Skipping tests because the site is small. Small sites can still have production-breaking changes.
  4. Overwriting the live directory. Release directories and a current symlink provide a cleaner rollback path.
  5. Ignoring database migrations. Application files and database state need separate deployment planning.
  6. Running deployments concurrently. Two simultaneous production releases can create race conditions.
  7. Trusting a successful SSH command. Always verify the application after deployment.
  8. Deploying every branch to production. Production should have an explicit release rule.
  9. Installing too many third-party actions. Every external action becomes part of your software supply chain.
  10. Having no rollback plan. If rollback requires an emergency server investigation, the deployment architecture is not finished.

Production-Ready VPS CI/CD Checklist

Before allowing automatic production deployment for a client, walk through this checklist.

Git repository is private where appropriate
Production branch is protected
CI tests run before deployment
Production environment is protected
Secrets are outside the repository
Deployment uses a dedicated Linux user
SSH access is restricted
Deployment does not run as root
Releases are stored separately
Previous releases are retained
Database backup exists
Rollback procedure has been tested
Post-deployment health check exists
Cache invalidation is defined
Deployment logs are accessible
Monitoring alerts the team

How to Scale the Pipeline Beyond One VPS

The architecture above works well for a small agency portfolio, but eventually you may outgrow a single VPS.

The important thing is that Git-based deployment does not have to disappear when infrastructure becomes more complex.

Stage Infrastructure model Deployment approach
Starter One VPS SSH + release directories
Growing agency Multiple VPS servers Environment-specific deployment targets
Containerized Docker hosts Build image + deploy container
High availability Multiple application nodes Rolling or controlled deployment
Large platform Container orchestration Deployment controller / orchestration platform

Docker Compose can also be used for production deployments on a single server. Docker's current production guidance recommends using production-specific Compose configuration where necessary and recreating the affected services after rebuilding them.

The key principle remains the same: build a predictable artifact, deploy it to a known environment, verify it, and maintain a recovery path.

How Agencies Should Organize CI/CD Across Client Projects

Once you manage multiple clients, the technical pipeline is only half the problem. You also need a consistent operating model.

Standardize the template

Start new projects from a known deployment template rather than writing a completely new workflow every time.

Separate environments

Use predictable staging and production conventions across client projects.

Document exceptions

If one client's architecture requires a special deployment process, document the reason instead of silently modifying the standard.

This approach becomes particularly valuable when a developer leaves the agency. The deployment system should remain understandable without depending on one person's memory.

For agencies that also offer hosting or white-label services, our guide to white-label reseller hosting platforms can help put the infrastructure side of the business into context.

Frequently Asked Questions About Git-Based CI/CD on a VPS

What is CI/CD in simple terms?

CI/CD is an automated process for checking, building, and deploying software. In a VPS workflow, Git stores the source code, CI runs automated checks, and the deployment stage publishes an approved version to the server.

Can I use GitHub Actions with a normal VPS?

Yes. A VPS does not need to be hosted by GitHub. A GitHub Actions workflow can connect to your VPS through a controlled deployment mechanism such as SSH.

Should I run a GitHub Actions self-hosted runner directly on the production VPS?

It can be done, but it increases the security and operational responsibility of the production machine. A separate runner or controlled SSH deployment model may provide stronger separation, depending on your architecture.

Should deployment happen automatically after every Git push?

Not necessarily. Automatic deployment can work well for low-risk staging environments. Production deployments for important client sites may benefit from protected branches, approvals, scheduled release windows, or other safeguards.

How do I roll back a failed VPS deployment?

If releases are stored in separate directories and the live website points to a current symlink, rollback can usually be performed by switching the symlink to the previous known-good release, subject to database and application compatibility.

Can this CI/CD method be used for WordPress?

Yes. Git works particularly well for custom themes, custom plugins, configuration templates, and other code. WordPress uploads and production databases require separate handling because they change independently of application source code.

Is GitLab CI/CD suitable for VPS deployment?

Yes. GitLab supports CI/CD pipelines, environments, deployment tracking, protected variables, and manual deployment controls. The same general architecture can be implemented with GitLab instead of GitHub.

Do I need Docker to use CI/CD on a VPS?

No. Docker is optional. A VPS can deploy traditional PHP, Node.js, Python, or other applications directly. Docker becomes useful when you want more consistent runtime environments and container-based release management.

What is the biggest benefit of CI/CD for a web agency?

The biggest operational benefit is repeatability. Instead of relying on manual server commands, the agency can define the deployment process once and use the same controlled workflow repeatedly.

Final Takeaway

A VPS does not have to mean manual deployment.

With Git, a CI service, controlled SSH access, release directories, automated tests, health checks, and a tested rollback procedure, a relatively simple VPS can support a professional deployment workflow for client websites.

The real advantage is not that someone can press a button instead of opening an SSH session. The advantage is that the deployment process becomes a documented system rather than a collection of habits.

For an agency, that distinction matters.

When a portfolio grows from five sites to fifty, consistency becomes more valuable than cleverness. The best CI/CD architecture is therefore the one your team can understand, secure, monitor, troubleshoot, and repeat across every client project.

Ready to Turn Your VPS Into a Deployment System?

Start with one client site, automate the complete build-test-deploy-verify cycle, document the rollback procedure, and then turn that workflow into your agency's reusable deployment template.

Explore More Web Hosting & Infrastructure Guides