Instead of manual FTP on every change — set up git push for automatic deployment. Takes 15 minutes, saves hours of work.
Deploying to a VPS via a Git bare repo
# On the VPS: create a bare repository
mkdir -p /var/repo/mysite.git
cd /var/repo/mysite.git
git init --bare
# Create the post-receive hook
nano hooks/post-receive
#!/bin/bash
GIT_WORK_TREE=/var/www/mysite git checkout -f main
echo "Deployed to /var/www/mysite"
chmod +x hooks/post-receive
# On your local machine:
git remote add production user@server-ip:/var/repo/mysite.git
git push production main
Deploying to cPanel hosting via Git
cPanel → Files → Git™ Version Control:
- Create → specify the path to the repository (
public_html) - Specify the GitHub/GitLab repository URL
- Click Update from Remote for a manual deploy
GitHub Actions: full auto-deploy
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_IP }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/mysite
git pull origin main
composer install --no-dev
php artisan migrate --force
Common Errors During Git Deployment
Below are the messages you'll actually run into when first setting up a bare repository and hooks, and what's behind them.
Permission denied (publickey).
fatal: Could not read from remote repository.
bash: hooks/post-receive: Permission denied
remote: error: hook declined to update refs/heads/main
error: failed to push some refs to 'server-ip:/var/repo/mysite.git'
fatal: 'production' does not appear to be a git repository
| Message | Cause | Fix |
|---|---|---|
| Permission denied (publickey) | The client's SSH key isn't added on the server, or the wrong key is being used | Check with ssh -T user@server-ip -i path_to_key, add the public key to ~/.ssh/authorized_keys |
| hooks/post-receive: Permission denied | The hook isn't marked executable — chmod +x was skipped | Run chmod +x hooks/post-receive and check the first line of the file (#!/bin/bash) |
| hook declined to update refs / failed to push some refs | The hook itself exited with a non-zero status — for example, git checkout -f failed due to permissions on the working directory | Run the hook manually: bash hooks/post-receive, fix the owner and permissions on /var/www/mysite |
| fatal: 'production' does not appear to be a git repository | Wrong remote URL or a typo in the path to the bare repository | Compare git remote -v with the actual path on the server — the directory must end with .git |
Comparing Deployment Methods
| Method | SSH required | Deploy speed | When to use it |
|---|---|---|---|
| Bare repo + post-receive hook | Yes, key-based access is required | Instant after git push | A VPS with full access, your own pipeline with no third-party services |
| cPanel Git Version Control | Not required, works through the web interface | Manual, via the Update from Remote button | Shared hosting, a developer without SSH access |
| GitHub Actions | Yes, for the SSH connection step to the server | Automatic on push to main, 1–3 minutes | A team that needs tests and a build step before deployment |
When Git Deployment Doesn't Fit
- The site stores user files (media library, uploads) right inside the repository's working directory —
git checkout -fduring deploy can touch them without a careful.gitignore - You need zero-downtime deployment with an instant rollback — plain
checkout -fdoesn't give you an atomic version swap; that needs a releases-plus-symlink scheme - The hosting has neither SSH nor a Git Version Control section in the panel — then plain FTP upload is the only option left
- Large binary files (video, database dumps) change regularly in the repository — Git handles these poorly without Git LFS, and LFS isn't available on every server
How to Verify the Deployment Succeeded
# On the server: compare the current commit with what was pushed locally
cd /var/repo/mysite.git && git log -1 --oneline
# In the working directory: file modification time should be fresh
stat -c '%y' /var/www/mysite/index.php
# The site responds without a 500 error
curl -I https://yourdomain.com/
- Compare the commit hash in the bare repository with what was pushed locally — they must match
- Check the file modification time in the working directory with
stat— it should update at the moment of deploy - Open the site with
curl -I: if the hook ran butcomposer installfailed, the files will be updated but the site will still be broken - In GitHub Actions, check the Actions tab — the latest workflow run must be green, even if
git pushcompleted without errors
Setting up key-based access instead of a password for the server itself is covered in the article on SSH access on shared hosting, and automating follow-up tasks after deploy is covered in the article on cron jobs in cPanel.
Frequently asked questions
How do you deploy without downtime and roll back instantly, since git checkout -f isn't atomic?
A plain checkout -f swaps files in the working directory, so the site briefly sees an incomplete set of files. For reliability, each release deploys into its own folder with a symlink pointing at the current version: the hook builds the release, verifies it, then flips the symlink. Rolling back means pointing the symlink at the previous folder.
What happens to files edited manually right on the server when the next git push runs?
The hook runs git checkout -f, which forcibly overwrites every tracked file with the version from the commit — any manual edit to config or code made directly on the server disappears without warning at the next deploy. User uploads and media are untouched only if they aren't tracked by the repository and are listed in .gitignore.
How do you roll back to a previous version of the site after a deploy breaks something?
On a VPS with a bare repository, switch the working directory to the needed commit with git checkout and rerun the hook steps manually, including composer install and migrations if needed. In cPanel, it's simpler to point Git Version Control at the earlier commit and click update from remote again. For an instant rollback, a releases-and-symlink scheme helps most.
Is it worth switching to GitHub Actions if git push through a bare repository already works?
A bare repository with a hook is faster and simpler for a single developer — deployment happens right after push, no external service involved. GitHub Actions pays off once a build, tests, or a verified composer install step are needed before going live: the pipeline halts on a build failure, while a plain hook applies the code regardless.