Panellicense

Deploy client sites with cPanel Git Version Control

Wire up push-to-deploy on cPanel with Git Version Control and a .cpanel.yml file — plus the dirty-tree, CageFS, and denyCurrentBranch gotchas that break real deploys.

6 min readUpdated 2026-07-13cpanel · git · deployment · cpanel-yml
schema: HowToschema: FAQPage

cPanel ships a Git host and a deploy runner in the box. Push a branch to a repository living in a customer's account, and a .cpanel.yml file copies the built site into the docroot — no FTP, no manual cp, no SSH babysitting. It's the closest thing to a real CI/CD flow you get on shared hosting, and most hosts never expose it to clients.

This is for agency engineers and hosts who deploy client sites onto cPanel and want a repeatable push-to-deploy pipeline. If you run Plesk, the Plesk Git deployment flow is the equivalent. The mechanics differ enough to matter.

Two ways Git Version Control runs

The feature (cPanel → Files → Git Version Control) does two unrelated jobs, and conflating them is where people get stuck.

  • Create a repository directly in the account. cPanel adds a post-receive hook, so every git push to it triggers a deployment. This is the push-to-deploy path.
  • Clone an existing remote (GitHub, GitLab) into the account. Cloned repositories do not auto-deploy on the origin's pushes — cPanel isn't the push target. You pull with Update from Remote, then run Deploy HEAD Commit manually, or script both over SSH.

Pick create-and-push if the developer controls the deploy cadence. Pick clone-and-pull if GitHub is the source of truth and a CI job drives the release.

Set up push-to-deploy

1. Create the repository outside the docroot

In Git Version Control → Create, set Clone URL empty (you're creating, not cloning), give it a Repository Path, and a name. Put it outside public_html:

/home/acmecorp/repositories/site

2. Add the cPanel remote and push

cPanel shows the clone URL after creation. From your workstation:

git remote add cpanel ssh://acmecorp@server.example.com/home/acmecorp/repositories/site
git push cpanel main

Use an SSH key loaded into the account (cPanel → SSH Access), not a password. If the first push throws remote: fatal: bad config value for 'receive.denyCurrentBranch' or the push is refused, cPanel's bundled Git binary isn't the one your client picked up. Force it:

git push cpanel HEAD --exec=/usr/local/cpanel/3rdparty/bin/git-receive-pack

3. Write .cpanel.yml

Deployment does nothing until a .cpanel.yml file sits in the repository root. It's YAML with one deployment key and a tasks list of bash commands run in order. Two-space indentation, no tabs — a tab anywhere silently aborts the deploy.

The tasks run with the working directory set to the repository's checked-out copy, as the cPanel user. There's no magic DEPLOYPATH handed to you — you define it:

---
deployment:
  tasks:
    - export DEPLOYPATH=/home/acmecorp/public_html
    - /bin/cp -R public/* $DEPLOYPATH
    - /bin/cp .htaccess $DEPLOYPATH

For a static site or a pre-built SPA, that's the whole file. Commit it, push, and cPanel runs the tasks on the post-receive hook. The Manage tab shows the last-deployed commit SHA and timestamp so you can confirm it fired.

What breaks real deploys

Dirty working tree. cPanel refuses to deploy a repository whose working tree has uncommitted changes. If a task writes into the repo checkout (a log, a cache file, a .env), the next deploy fails with a dirty-tree error. Write only into $DEPLOYPATH, never back into the repo.

CageFS-jailed commands. On a CloudLinux box, tasks run inside the user's CageFS. Only binaries proxied into the cage exist — rsync, composer, or a custom Node build tool may be missing even though they're on the host. Test each command over SSH as the user first; if it's absent, add it to CageFS or drop it from the task list.

Non-interactive only. Tasks can't prompt. Anything that waits on stdin — a password, a [y/N] confirmation, an SSH host-key prompt — hangs the deploy until it times out. Add -y, --no-interaction, or StrictHostKeyChecking=no where needed.

CRLF line endings. Edit .cpanel.yml on Windows and the CRLF endings make the Linux YAML parser choke. Force LF (git config core.autocrlf input, or an .gitattributes rule).

App restarts. Copying files into the docroot doesn't reload a running app. For a Node.js app under Passenger, touch the restart trigger as the last task; for PHP with OPcache, clear the cache so the new code is actually served:

    - /bin/mkdir -p $DEPLOYPATH/tmp && /usr/bin/touch $DEPLOYPATH/tmp/restart.txt

Staging and production from one repo

Keep a single .cpanel.yml and source the environment-specific paths from a config file that lives in each account (not in Git), so the same commit deploys to staging and production without a branch-per-environment mess:

---
deployment:
  tasks:
    - source /home/acmecorp/deploy.cfg
    - /bin/cp -R public/* $DEPLOYPATH

deploy.cfg on the staging account exports DEPLOYPATH=/home/acmecorp/staging.acmecorp.com; on production it exports the live docroot. Same repo, same YAML, different target — and secrets stay out of version control.

Driving it from GitHub

If GitHub is canonical, don't push to cPanel by hand. In a GitHub Actions workflow, SSH into the account on merge to main, cd into the cloned repo, git pull, and trigger the deploy. cPanel exposes the deploy step as a UAPI call you can run over SSH:

uapi VersionControlDeployment create repository_root=/home/acmecorp/repositories/site

Scope the Actions SSH key to that one account. For anything that touches WHM-level automation instead, mint a scoped WHM API token rather than reusing root.

FAQ

Why does my cPanel Git deployment fail with a dirty working tree?+
A deploy task wrote a file back into the repository checkout — a log, cache, or generated .env. cPanel won't deploy a repo with uncommitted changes. Make tasks write only into your deploy target ($DEPLOYPATH), never into the repository directory.
Does a push to GitHub auto-deploy to cPanel?+
No. Only repositories created directly in cPanel get the post-receive hook that triggers deployment. A repo cloned from GitHub deploys only when you run Update from Remote and Deploy HEAD Commit, or script those over SSH from a CI job.
Where do I put the .cpanel.yml file?+
In the top-level directory of the repository, committed to the branch you deploy. It needs a deployment key with a tasks list, two-space indented, no tabs. Without it, cPanel has nothing to run and the deploy is a no-op.
Why does my push get a denyCurrentBranch error?+
Your local Git is calling the wrong receive-pack. Push with --exec=/usr/local/cpanel/3rdparty/bin/git-receive-pack to force cPanel's bundled Git binary, which is configured to accept pushes to the checked-out branch.
Can I run npm build or composer install during deployment?+
You can, but don't. Deploy tasks run inside CageFS where those binaries may be missing, they're slow, and a failed build leaves a half-copied docroot. Build before committing or in CI, and let .cpanel.yml only copy the artefacts.

Next steps

Running these pipelines for paying clients? Keep the panel legitimately licensed — activate a cPanel license or talk to sales about volume tiers.

Switch in an afternoon

Switch from your current reseller — free.

We migrate active cPanel, Plesk, LiteSpeed and CloudLinux licenses from any reseller. We prorate the first month so you never pay twice, and your customers see zero downtime during the swap.