Skip to content
A AhsanLab.Tech
Deployment & CI/CD 14 min read · August 22, 2026

Automating cPanel Deploys With Git: .cpanel.yml and What Actually Works

cPanel has had built-in Git deployment for years and almost nobody uses it, because the first thing everyone tries — push to GitHub and expect the site to update — is the one thing it does not do on its own. The working setup, the three gotchas that stop people, and an honest comparison against FTP.

A Ahsan Habib Save

cPanel has had built-in Git deployment for years and almost nobody uses it, because the documentation describes the buttons rather than the workflow, and the first thing everyone tries — push to GitHub, expect the site to update — is the one thing it does not do on its own.

It is genuinely good once you understand what it is: a bare Git repository on your host, a checkout, and a YAML file listing shell commands to run after that checkout. This post is the working setup, the three gotchas that stop people, and the honest comparison against just using FTP.

The model

What cPanel Git actually does

The mental model that makes everything else obvious: cPanel gives you a repository on the server and a button that means "fetch, check out, then run my script". It is not a webhook receiver, and it does not watch GitHub.

GITHUB origin/main cPANEL · GIT VERSION CONTROL ~/repositories/app · a clone ↓ git fetch && git checkout ↓ reads .cpanel.yml ↓ runs your commands pull public_html/ the document root rsync, not serve .env · storage/ already there · never touched NOT INCLUDED composer npm / node any compiler often, on most plans THE MISSING PIECE EVERYONE HITS cPanel does NOT watch GitHub. Pushing to origin changes nothing until something tells cPanel to pull. the trigger is a GitHub Action that pushes to the cPanel repo, or an SSH call that pulls — see below
FIGURE 1 — THE MODEL · A repo on the host, a checkout, and a list of shell commands. Everything confusing about this feature comes from expecting a fourth thing that is not there.

Setup

The four steps that work

  1. Create the repository in cPanel, cloning from GitHub

    Git Version Control › Create, tick "Clone a Repository", and give it an SSH URL. Put it in ~/repositories/app — not in public_html. A repository inside the document root means /.git/ is downloadable and your entire source history is public.

  2. Add the host's SSH key to GitHub as a deploy key

    cPanel generates a key under SSH Access. Add the public half to the GitHub repo as a read-only deploy key, not to your account. Read-only is enough: the server only ever pulls.

  3. Commit a .cpanel.yml at the repository root

    This is the whole deployment. Without it the Deploy button is greyed out, which is the second most common reason people give up on this feature.

  4. Trigger it from CI instead of pressing the button

    The button works and is fine for a small site. For push-to-deploy, a GitHub Action calls the host over SSH to pull and deploy.

.cpanel.yml — YAML, and the indentation is not negotiable
---
deployment:
  tasks:
    - export DEPLOYPATH=/home/myuser/public_html
    - export REPO=/home/myuser/repositories/app

    # Application code. -a preserves timestamps; --delete keeps the doc root
    # in sync with the repo, so removed files actually disappear.
    - /bin/rsync -a --delete
        --exclude='.git'
        --exclude='.env'
        --exclude='storage/app'
        --exclude='storage/logs'
        --exclude='node_modules'
        $REPO/ $DEPLOYPATH/

    # Laravel caches, using the host's PHP binary — `php` may be the wrong one.
    - /usr/local/bin/ea-php82 $DEPLOYPATH/artisan migrate --force
    - /usr/local/bin/ea-php82 $DEPLOYPATH/artisan config:cache
    - /usr/local/bin/ea-php82 $DEPLOYPATH/artisan route:cache
    - /usr/local/bin/ea-php82 $DEPLOYPATH/artisan view:cache

    # Belt and braces: make sure the repo is not reachable over HTTP.
    - /bin/chmod 700 $REPO
Three ways this file silently does nothing It must be at the repository root, committed to the branch being deployed, and use spaces, never tabs — cPanel's parser rejects tabs without a useful message. If the Deploy button stays greyed out, it is one of those three every time.

The gotchas

Three things that stop people

1. There is no composer, and no node

Most shared plans have neither. Some have a composer binary at a strange path; almost none have a Node toolchain. So vendor/ and your built assets cannot be produced on the host.

Tempting

Commit vendor/ and public/build/ to the repository.

It works. It also means every dependency update is a huge diff, your repo grows without limit, and dev dependencies ship to production unless you are careful every single time.

Better

Build in CI and push the result to a separate deploy branch that cPanel tracks.

main stays clean source. deploy is source plus vendor/ plus built assets, written only by the pipeline, never by a person.

.github/workflows/cpanel.yml — build, commit to a deploy branch, tell cPanel
name: Deploy to cPanel
on:
  push:
    branches: [main]

concurrency: { group: cpanel-deploy, cancel-in-progress: false }

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with: { php-version: '8.2' }
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }

      - run: composer install --no-dev --optimize-autoloader --prefer-dist
      - run: npm ci && npm run build
      - run: php artisan test

      - name: Publish the built tree to the deploy branch
        run: |
          # vendor/ and public/build are gitignored on main — force them here.
          git config user.name  "ci"
          git config user.email "ci@example.com"
          git checkout -B deploy
          git add -A -f vendor public/build
          git add -A
          git commit -m "build ${GITHUB_SHA::7}" || echo "nothing to commit"
          git push -f origin deploy

      - name: Ask cPanel to pull and deploy
        run: |
          install -m 700 -d ~/.ssh
          echo "${{ secrets.CPANEL_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          echo "${{ secrets.CPANEL_KNOWN_HOSTS }}" > ~/.ssh/known_hosts
          ssh -p ${{ secrets.CPANEL_PORT }} ${{ secrets.CPANEL_USER }}@${{ secrets.CPANEL_HOST }} \
            'cd ~/repositories/app && git fetch origin deploy && git reset --hard origin/deploy \
             && /usr/local/cpanel/scripts/cpuser_service ensure_deploy 2>/dev/null; \
             /bin/bash -lc "cd ~/repositories/app && git checkout deploy"'

If your plan has no SSH at all, the fallback is the cPanel API's VersionControlDeployment::create endpoint, called with curl and an API token — same effect, one more secret to manage.

2. The document root is not the repository

The repository lives in your home directory; the site is served from public_html. The rsync line in .cpanel.yml is what bridges them, and two flags on it matter more than they look.

--delete makes the document root match the repository, so a file you removed in Git actually disappears from the server. Without it, deleted files linger forever — including old controllers and, memorably, an info.php somebody added during debugging in 2023. And every --exclude is protecting something that must survive: .env, uploads, logs. Get the exclude list right before the first deploy, because --delete without it will remove your production environment file on the very first run.

3. Laravel expects public/ to be the web root

cPanel serves public_html. The clean answer, if the panel allows it, is to point the domain's document root at public_html/public and rsync the whole application into public_html. If it does not allow it, the common workaround is rsyncing public/ to the root and the rest one level up — which works, and means your index.php paths need editing, which is a change you must remember on every future deploy. Prefer the document root change.

Honesty

Is this better than FTP?

cPanel GitFTP sync
Post-deploy commandsYes — migrations, cachesNo
Deletes removed filesYes, with --deleteOnly if configured
RollbackCheckout an older commitRedeploy an old build
SpeedFast — one fetch, local rsyncSlow — many small transfers
Needs SSH to automateYes (or the API)No
Repo sizeGrows with built assetsNot applicable
AtomicNo — rsync is file by fileNo

Two rows decide it. Post-deploy commands mean migrations and cache warming happen as part of the deploy rather than as a thing you remember to do — that alone is worth the setup for any framework app. And the rsync happens locally on the server, so a deploy is seconds rather than the minutes an FTP sync takes over the network.

Neither is atomic. There is still a window where the site is half-updated — cPanel Git just makes that window a few seconds of local file copying instead of several minutes of transatlantic FTP.

If you want that window closed, the maintenance-flag trick from the FTP approach works here too: touch the flag as the first task in .cpanel.yml and remove it as the last.

What it still will not do

  • No atomic swap. No symlink flip, because you cannot repoint the document root from a script.
  • No daemons. Everything from the shared-hosting constraints still applies — queues run on cron, no WebSockets.
  • Rollback needs a re-deploy of an older commit, not an instant switch.
  • The deploy log is in the cPanel UI and is not especially good. A failing .cpanel.yml task often surfaces as a generic failure.
  • Repo size grows with every built-asset commit on the deploy branch. Force-pushing keeps only one commit of history there, which is why the workflow above uses -f.

The checklist

  • Repository in ~/repositories/, never inside public_html.
  • Read-only deploy key on the GitHub repo, not an account key.
  • .cpanel.yml at the repo root, on the deployed branch, spaces not tabs.
  • Exclude .env, storage/app, storage/logs, .git before the first --delete run.
  • --delete on the rsync, so removed files actually go.
  • Explicit PHP binary path, never bare php.
  • Build in CI, push to a deploy branch — do not commit vendor/ to main.
  • Document root at public_html/public if the panel allows it.
  • A trigger — an Action over SSH or the cPanel API. Nothing is automatic by default.
  • Maintenance flag first task and last task, if the half-updated window matters.

Where to start on Monday

Open cPanel and look for Git Version Control and SSH Access. If both are present, you can have push-to-deploy with migrations running automatically by the end of the afternoon, which is a considerable upgrade over dragging folders — and a great deal of what people assume requires leaving shared hosting entirely.

  • .cpanel.yml
  • deploy branch
  • rsync --delete
  • deploy key
  • ea-php82
  • document root
  • cPanel API
  • maintenance flag

The pure-FTP version of this problem is here, the cron patterns for queues are in the same post, and if you find yourself wanting an atomic swap badly enough, that is the argument for moving to a VPS. The wider decision between the two is in the CI/CD guide.

#CI/CD #GitHub Actions #VPS #Laravel #Web Apps

Comments (0)

No comments yet

Be the first to share a thought on this article.

Join the conversation

Comments are moderated before they appear.

Keep reading

Related articles