Deploying a Sage WordPress Theme to cPanel
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_keyThis 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.pubIn 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-ip2. 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 pnpm2.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 --version2.3 Check PHP Version
Sage requires PHP 8.2+. Check:
php -vIf 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.pubCopy the output.
3.2 Configure SSH to Use the Deploy Key
nano ~/.ssh/configAdd:
Host github.com
IdentityFile ~/.ssh/github_deploy
StrictHostKeyChecking noFix permissions:
chmod 600 ~/.ssh/config3.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.comExpected 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-branch4.2 Set Up Branch Tracking
git branch -m master your-branch
git branch --set-upstream-to=origin/your-branch your-branchNow 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 buildlocally 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 publicRemove 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_keyCopy 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-nameNo 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-nameGo 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 build3. Commit and push:
git add .
git commit -m "your change description"
git push origin your-branch-name4. 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 installon 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-nameSSH Config Permission Error
- Symptom: "Bad owner or permissions on ~/.ssh/config"
- Fix:
chmod 600 ~/.ssh/configComposer 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