HSHimanshu Sharma
PHP

Deploy code from Git to shared hosting: cPanel, SSH keys and safe releases

Deploy code from Git to shared hosting: cPanel, SSH keys and safe releases Advertisement

If you still deploy a website by dragging files into an FTP window, you have probably overwritten a good file with an old one at least once. Deploying from Git fixes that. The code lives in a repository, every change is a commit you can inspect, and putting it on the server is one command that is the same every time. This guide shows how to deploy a PHP site (plain PHP or Laravel) from Git to shared hosting that offers cPanel or SSH access, using a production branch and SSH keys. It also covers how to keep secrets out of the repository, how to roll back, and a checklist for Laravel.

How this was tested. I tested all the Git parts locally: a bare "server" repository, a work repository, a production branch, a post-receive hook that deploys on push, an SSH key pair, and a rollback with git revert. I do not have a cPanel account in this session, so the cPanel screens are described from its documented behaviour, and their labels may differ on your host. Some Hostinger plans use their own panel (hPanel) instead of cPanel, with similar features under different names; check what yours offers.

What you need

  1. Hosting with SSH or a Git feature. cPanel's Git Version Control tool (when your host enables it) or SSH access to the account.
  2. A Git repository on GitHub, GitLab or Bitbucket (any version control service works), or a private repository on the server itself.
  3. Basic Git skills: add, commit, push, pull, branch, merge.
  4. A domain connected to the hosting, so you can see the result.

The workflow in one picture

your computer          Git service            server
write code  --push-->  repository  --pull-->  /home/you/public_html
 (main branch)         (main, production)      (production branch)

You develop on main. When a version is ready, you merge it into a production branch. The server only ever follows production. This gives you a clear question to answer at any time: "what is live right now?" Answer: the tip of production.

Step 1: set up the repository

On your computer:

git clone git@bitbucket.org:you/mysite.git
cd mysite
# create files, then:
git add .
git commit -m "first version"
git push origin main

Add a .gitignore before the first commit so secrets and generated folders never enter the history:

.env
vendor/
node_modules/
storage/logs/*
*.sqlite

If a password or key is ever committed, treat it as leaked: change it. Removing the file in a later commit does not remove it from history.

Step 2: create the production branch

git checkout -b production
git push -u origin production

Now the repository has main and production (the test showed * production with origin/main and origin/production). To release later:

git checkout main
# ...work, commit, push...
git checkout production
git merge main          # bring the tested changes in
git push origin production
git checkout main       # go back to work

If production only ever receives merges from main, the merge is a simple fast-forward, and git log --oneline --graph --all shows a straight line of commits.

Step 3: an SSH key so the server can read your repository

If the repository is private, the server needs permission to pull from it. Do not use your own password or personal key. Create a dedicated deploy key on the server over SSH:

ssh-keygen -t ed25519 -C "deploy@yoursite" -f ~/.ssh/deploy_key -N ""
cat ~/.ssh/deploy_key.pub

I generated one this way; the public key is a single line starting with ssh-ed25519 AAAA..., and ssh-keygen -lf shows its fingerprint (SHA256:...). Then:

  1. Copy the public key (deploy_key.pub) into your repository's settings as a deploy key (Bitbucket: Repository settings > Access keys; GitHub: Settings > Deploy keys; GitLab: Settings > Repository > Deploy keys). Keep it read-only. The server only needs to pull.
  2. Keep the private key (deploy_key, no .pub) on the server only. Never commit it or paste it into chat.
  3. Tell SSH to use it for that host, in ~/.ssh/config on the server:
Host bitbucket.org
  IdentityFile ~/.ssh/deploy_key
  IdentitiesOnly yes
  1. Test it: ssh -T git@bitbucket.org should greet you by repository or account name.

In cPanel's Git Version Control screen, there is usually a "Public SSH key" section that generates the key for you; copy it to your repository's access keys the same way.

Step 4: put the code on the server

You have three common ways. Pick one.

Option A: cPanel Git Version Control

In cPanel, open Git Version Control > Create, paste the repository's SSH clone URL, choose a path (for example /home/you/repositories/mysite) and create it. cPanel clones the repository and lets you pick a branch (choose production). It can then Update from Remote (a git pull) and Deploy HEAD Commit, which copies files into the live folder according to a file named .cpanel.yml in the repository root:

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

Use full paths for commands (/bin/cp), replace YOURUSER with your cPanel username, and keep the tasks short and safe: they run on every deploy. Two limitations to know: cPanel only deploys when the working tree is clean (no local changes on the server), and .cpanel.yml must be committed on the branch you deploy.

Option B: SSH and git pull

If you have SSH, the simplest deploy is to keep a clone on the server and pull:

ssh you@yourserver
cd ~/repositories/mysite
git pull origin production

I tested this: a clone of the production branch, a new commit pushed from the work copy, and git pull on the "server" copy fast-forwarded and added the new file. To make the web root the clone itself, point the domain's document root at the public folder of the clone (many hosts let you change the document root per domain), so no copy step is needed.

Option C: push to deploy with a post-receive hook

If the server hosts a bare repository, a post-receive hook can check out the branch into the live directory every time you push. This makes git push origin production the whole deploy.

#!/bin/sh
# remote.git/hooks/post-receive   (chmod +x)
TARGET=/home/you/public_html
while read old new ref; do
  if [ "$ref" = "refs/heads/production" ]; then
    git --work-tree=$TARGET --git-dir=/home/you/remote.git checkout -f production
    echo "deployed production to $TARGET"
  fi
done

In my test, after git push origin production the push output printed remote: deployed production to ..., and the live folder contained the new index.html. A push to main printed nothing and deployed nothing, because the hook only reacts to production.

Step 5: keep secrets and uploads out of Git

The repository holds code. The server holds configuration and data:

  • .env or config.php with database passwords: create it once on the server by hand. Never commit it; commit an .env.example with fake values so everyone knows what is needed.
  • User uploads and generated files (storage/app, uploads/): ignore them in Git, and note that a deploy that replaces the whole folder would delete them. Deploy the code over the top rather than wiping the folder.
  • Different settings per environment come from the environment file, not from if ($_SERVER['HTTP_HOST'] == ...) code.

Step 6: roll back when a release breaks

Because every release is a commit, going back is a Git operation. The safest method is git revert, which adds a new commit that undoes the bad one, so history stays honest and a normal push redeploys:

git checkout production
git revert --no-edit HEAD      # undo the last commit
git push origin production     # deploys the undone state

In the test, the live page showed v3 after a release; after the revert and push, the hook redeployed and the page showed v2 again. For a bigger rollback, use git revert -m 1 <merge-commit> for a merge, or deploy a tag: tag every release (git tag -a v1.0 -m "release"; git push --tags) and you can always check out v1.0 on the server. Database changes are not undone by Git, so migrations need their own plan: write reversible migrations and take a backup before a release.

A Laravel checklist

Laravel on shared hosting needs a few extra steps after the code arrives:

  1. Point the domain at public/, not the project root, so .env, app/ and vendor/ are not reachable from the web. If the host cannot change the document root, move the contents of public/ into the web root and adjust the paths in index.php; the first option is much safer.
  2. Install dependencies on the server: composer install --no-dev --optimize-autoloader. Do not commit vendor/.
  3. Environment: copy .env.example to .env, set APP_ENV=production, APP_DEBUG=false, the database values, and run php artisan key:generate once.
  4. Migrate: php artisan migrate --force.
  5. Cache for speed: php artisan config:cache, route:cache, view:cache. Run php artisan config:clear first if you change .env.
  6. Permissions: storage and bootstrap/cache must be writable by the web user.
  7. Front-end assets: run npm run build on your computer or in CI and deploy the built files, as shared hosting often has no Node.js.

You can put steps 2, 4 and 5 into a small deploy.sh and run it after each pull, or call it from the hook.

Common problems

Symptom Likely cause and fix
Permission denied (publickey) the deploy key is not added to the repository, or SSH is using a different key; check ~/.ssh/config and ssh -vT git@host
cPanel "Deploy HEAD Commit" is greyed out .cpanel.yml is missing, uncommitted, or the working tree has local changes
Site shows the old version you deployed main instead of production, or the server is on a different branch; git branch shows the current one
500 error after deploy missing .env, wrong PHP version, no vendor/, or unwritable storage; read the error log
Uploaded images vanished a deploy wiped the upload folder; keep it out of the deploy path
Merge conflicts on the server someone edited files directly on the server; never edit live files, change code in Git
.env is public the document root is the project folder, not public/; fix it immediately and rotate every secret in it

Good habits

  1. Never edit code on the server. If you must hot-fix, commit it to Git afterwards, or the next deploy will undo it.
  2. Deploy small, deploy often. Small releases are easy to understand and to revert.
  3. Test before production. Run your tests on main; only merge green builds.
  4. Back up the database and uploads before risky releases.
  5. Tag releases so you can always name what was live.
  6. Automate when ready. A CI service (GitHub Actions, Bitbucket Pipelines, GitLab CI) can run tests and then deploy by SSH when you merge into production.

Next steps

If you are new to PHP itself, the free PHP course takes you from the first script to a database-driven site, and the course overview shows how the lessons fit together. Experienced developers can test themselves with the PHP interview questions. For authorisation in a Laravel project you are about to deploy, read roles and permissions in Laravel.

Advertisement