Deploying a Sage WordPress Theme to cPanel

2026-05-16  ·  10 min read

← Back to Writing

1. Setting Up SSH Access to cPanel

1.1 Generate an SSH Key Locally

Run this on your local machine:

ssh-keygen -t rsa -b 4096 -f ~/.ssh/cpanel_key

This creates two files:

  • ~/.ssh/cpanel_key — private key, stays on your machine only
  • ~/.ssh/cpanel_key.pub — public key, this gets uploaded to cPanel

1.2 Import the Public Key into cPanel

cat ~/.ssh/cpanel_key.pub

In cPanel → Security → SSH Access → Manage SSH Keys → Import Key. Paste the output into the Public Key field. Leave the private key field empty. After importing, click Authorize next to the key.

> Never paste your private key anywhere. It stays on your machine only.

1.3 Connect via SSH

ssh -i ~/.ssh/cpanel_key -p YOUR_SSH_PORT username@your-server-ip

2. Installing Required Tools on the Server

Shared hosting does not give you sudo. All tools must be installed in your home directory.

2.1 Install Node.js via NVM

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
source ~/.bashrc
nvm install --lts
npm install -g pnpm

2.2 Install Composer Without sudo

mkdir -p $HOME/bin
curl -sS https://getcomposer.org/installer | php -- --install-dir=$HOME/bin --filename=composer
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc
composer --version

2.3 Check PHP Version

Sage requires PHP 8.2+. Check:

php -v

If below 8.2, go to cPanel → MultiPHP Manager, select your domain, switch to PHP 8.2 or 8.3.

> Composer errors saying "Your dependencies require PHP >= 8.2.0" are a PHP version problem, not a Composer version problem.

3. Connecting the Private GitHub Repository

3.1 Generate a Deploy Key on the Server

SSH into your server and run:

ssh-keygen -t ed25519 -C "cpanel-deploy" -f ~/.ssh/github_deploy -N ""
cat ~/.ssh/github_deploy.pub

Copy the output.

3.2 Configure SSH to Use the Deploy Key

nano ~/.ssh/config

Add:

Host github.com
  IdentityFile ~/.ssh/github_deploy
  StrictHostKeyChecking no

Fix permissions:

chmod 600 ~/.ssh/config

3.3 Add the Deploy Key to GitHub

Go to your repository → Settings → Deploy keys → Add deploy key. Paste the public key. Read-only access is sufficient.

> If deploy keys are disabled by your GitHub organization policy, use a Personal Access Token instead. Generate one under your GitHub profile → Settings → Developer settings → Personal access tokens (classic) with repo scope, then clone using:

> git clone https://TOKEN@github.com/org/repo.git

3.4 Test the Connection

ssh -T git@github.com

Expected output: Hi org/repo! You've successfully authenticated, but GitHub does not provide shell access.

4. Setting Up Git on the Server

Initialize git at the WordPress root directory, not inside the theme folder. This way git manages your tracked files while leaving WordPress core files untouched.

4.1 Initialize Git at the WordPress Root

cd ~/your-site-directory
git init
git remote add origin git@github.com:YourOrg/your-repo.git
git fetch origin
git reset --hard origin/your-branch

4.2 Set Up Branch Tracking

git branch -m master your-branch
git branch --set-upstream-to=origin/your-branch your-branch

Now git pull will work without specifying the branch every time.

4.3 Verify Theme Files

ls wp-content/themes/

Confirm your Sage theme folder is present with composer.json, package.json, and vite.config.js.

5. Handling Shared Hosting Memory Limits

This is the most important architectural decision in the setup.

Shared hosting kills processes that exceed memory limits. Running pnpm install on the server to install 100+ Node packages will get your process SIGKILL'd before it finishes.

The Solution: Build Locally, Deploy Artifacts

Build on your local machine and commit the compiled assets to git. The server only does a git pull — no Node, no npm, no build process on the server.

  • Run pnpm run build locally before every push
  • The public/build/ folder (compiled CSS, JS, asset manifest) gets committed to the repo
  • The server pulls these pre-built files — no compilation needed

Update .gitignore

Make sure .gitignore does not exclude the public/build directory:

cat .gitignore | grep public

Remove any line ignoring the build output. The built assets need to be in the repo.

> This is a trade-off — your repo will include compiled files. The alternative is a dedicated build server or a higher-tier hosting plan with more RAM. For shared hosting, this is the practical solution.

6. GitHub Actions CI/CD Workflow

6.1 Add the SSH Private Key to GitHub Secrets

Go to your repository → Settings → Secrets and variables → Actions → New repository secret:

  • Name: SSH_PRIVATE_KEY
  • Value: full contents of your local private key file
cat ~/.ssh/cpanel_key

Copy everything including the BEGIN and END lines.

6.2 Create the Workflow File

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

name: Deploy to cPanel

on:
  push:
    branches: [your-branch-name]

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v4

      - name: Deploy via SSH
        uses: appleboy/ssh-action@v1
        with:
          host: YOUR_SERVER_IP
          port: YOUR_SSH_PORT
          username: YOUR_CPANEL_USERNAME
          key: ${{ secrets.SSH_PRIVATE_KEY }}
          script: |
            cd ~/your-site-directory
            git pull origin your-branch-name

No build steps in the script — the server only pulls. All compilation happens locally before the push.

6.3 Push the Workflow

git add .github/workflows/deploy.yml
git commit -m "add deployment workflow"
git push origin your-branch-name

Go to GitHub → Actions tab to watch the workflow run.

7. Day-to-Day Deploy Flow

Once set up, deploying is four steps:

1. Make your changes locally

2. Build the theme assets:

pnpm run build

3. Commit and push:

git add .
git commit -m "your change description"
git push origin your-branch-name

4. GitHub Actions triggers automatically, SSHes into the server, and pulls the changes. Done.

> Always run pnpm run build before pushing. If you push source changes without rebuilding, the live site will have stale compiled assets.

8. Handling Media and Uploads

The wp-content/uploads/ folder is never tracked in git. When migrating from local to live for the first time, transfer it separately:

scp -P YOUR_SSH_PORT -r /path/to/local/wp-content/uploads/ username@server-ip:~/your-site/wp-content/uploads/

After the initial migration, uploads made on the live site stay on the server. Your git workflow only manages theme files and tracked configuration.

9. Troubleshooting Reference

Plugin Installation Failing in WP Admin

  • Symptom: "Could not create directory" error
  • Cause: Missing plugins folder or wrong ownership
  • Fix:
mkdir -p wp-content/plugins
sudo chown -R $(whoami):$(whoami) wp-content/

pnpm install Killed on Server

  • Symptom: Process ends with SIGKILL (Forced termination)
  • Cause: Shared hosting memory limit exceeded
  • Fix: Build locally and commit compiled assets. Never run pnpm install on the server.

git pull Asks for Branch Specification

  • Symptom: "There is no tracking information for the current branch"
  • Fix:
git branch --set-upstream-to=origin/branch-name branch-name

SSH Config Permission Error

  • Symptom: "Bad owner or permissions on ~/.ssh/config"
  • Fix:
chmod 600 ~/.ssh/config

Composer PHP Version Error

  • Symptom: "Your dependencies require PHP >= 8.2.0"
  • Cause: Server CLI is running an older PHP version
  • Fix: Switch PHP version in cPanel MultiPHP Manager to 8.2 or 8.3