Table of Contents
Git deployment for PHP projects solves every problem that FTP uploads create. No record of what changed or when. No way to roll back when something breaks. Getting code from your local machine to a production server is where most PHP deployment problems happen – and git deployment PHP workflows fix all of them with tags, hooks and automation.
Git solves every one of these problems. This guide covers deploying PHP applications with Git – pulling to a server, using tags for versioning, Git hooks for automation, and the basics of CI/CD pipelines.
For official Git deployment documentation see Git server deployment documentation.
The Simplest Git Deployment
The most basic Git deployment: your server has a clone of the repository and you pull when you want to deploy:
# On your server (via SSH)
ssh user@yourserver.com
# Navigate to the web root
cd /var/www/html/your-php-project
# Pull the latest code from main
git pull origin main
# Install/update dependencies
composer install --no-dev --optimize-autoloader
# Clear any application cache (Laravel example)
php artisan config:clear
php artisan cache:clear
php artisan view:clear
Simple and it works for solo projects. The problems appear when you’re deploying frequently, working in a team, or need to know exactly what version is on the server at any time. That’s where tags and automation come in.
Git Deployment PHP: Deploying Specific Versions With Tags
Tags give you named, reproducible points in history. Instead of deploying “whatever is on main” you deploy a specific tagged version:
# On your development machine - create and push a release tag
git tag -a v1.3.0 -m "Release v1.3.0 - Add product search and email notifications"
git push origin v1.3.0
# On the server - deploy the specific tagged version
ssh user@yourserver.com
cd /var/www/html/your-php-project
# Fetch all tags from remote
git fetch --tags
# Check what version is currently deployed
git describe --tags
# Deploy the new version
git checkout v1.3.0
# Install dependencies for this exact version
composer install --no-dev --optimize-autoloader
Output of git describe –tags:
v1.2.0
Now you know exactly what’s on the server. If v1.3.0 breaks production:
# Roll back to the previous version in seconds
git checkout v1.2.0
composer install --no-dev --optimize-autoloader
php artisan config:clear
Checking What Will Deploy Before Deploying
# On the server - what commits are in the new version
# that aren't on the server yet?
git fetch origin
# Compare current HEAD to what you're about to pull
git log HEAD..origin/main --oneline
# What files will change?
git diff HEAD..origin/main --name-only
Output:
b5c6d7e Add product search filter with pagination
a1b2c3d Add SMTP email notification system
3f7a9b2 Update Composer dependencies
If composer.json is in the diff you know to run composer install. If migration files changed you know to run php artisan migrate. No more guessing what changed between deployments.
Git Hooks for Deployment Automation
Git hooks are scripts that run automatically at specific points in the Git workflow. The two most useful for deployment:
post-receive Hook (Server-Side)
This hook runs on the server after it receives a push. Used to automatically deploy when you push to a specific branch:
# On the server - set up a bare repository
mkdir /var/repos/your-php-project.git
cd /var/repos/your-php-project.git
git init --bare
# Create the post-receive hook
nano hooks/post-receive
#!/bin/bash
# hooks/post-receive
TARGET="/var/www/html/your-php-project"
GIT_DIR="/var/repos/your-php-project.git"
BRANCH="main"
while read oldrev newrev ref
do
DEPLOYED_BRANCH=$(git rev-parse --symbolic --abbrev-ref $ref)
if [ "$DEPLOYED_BRANCH" == "$BRANCH" ]
then
echo "Deploying branch: $DEPLOYED_BRANCH"
git --work-tree=$TARGET --git-dir=$GIT_DIR checkout -f $BRANCH
cd $TARGET
echo "Installing Composer dependencies..."
composer install --no-dev --optimize-autoloader --quiet
echo "Clearing application cache..."
php artisan config:clear
php artisan cache:clear
php artisan view:clear
php artisan route:clear
echo "Running migrations..."
php artisan migrate --force
echo "Deployment complete."
fi
done
# Make the hook executable
chmod +x hooks/post-receive
# On your development machine - add the server as a remote
git remote add production user@yourserver.com:/var/repos/your-php-project.git
# Deploying is now just a push
git push production main
Output when you push:
Counting objects: 12, done.
Writing objects: 100% (12/12), done.
remote: Deploying branch: main
remote: Installing Composer dependencies...
remote: Clearing application cache...
remote: Running migrations...
remote: Deployment complete.
To user@yourserver.com:/var/repos/your-php-project.git
a1b2c3d..b5c6d7e main -> main
One git push deploys everything. No SSH-ing into the server manually, no forgetting to run migrations, no missing the cache clear.
pre-commit Hook (Local)
Runs before every commit on your local machine. Use it to catch problems before they reach the repository:
# In your project: .git/hooks/pre-commit
#!/bin/bash
echo "Running pre-commit checks..."
# Check PHP syntax on all changed PHP files
CHANGED_PHP=$(git diff --cached --name-only --diff-filter=ACM | grep '\.php$')
if [ -n "$CHANGED_PHP" ]; then
for file in $CHANGED_PHP; do
php -l "$file" > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo "PHP syntax error in: $file"
echo "Commit aborted."
exit 1
fi
done
echo "PHP syntax check passed."
fi
# Prevent committing debug statements
if git diff --cached | grep -E '(var_dump|dd\(|die\(|print_r\()' > /dev/null; then
echo "Found debug statements (var_dump/dd/die/print_r) in staged changes."
echo "Remove them before committing."
exit 1
fi
echo "All checks passed."
exit 0
chmod +x .git/hooks/pre-commit
Now if you try to commit PHP with syntax errors or debug statements:
git commit -m "Add payment integration"
# Output:
Running pre-commit checks...
PHP syntax error in: src/Payment/PaymentController.php
Commit aborted.
The commit is blocked until the error is fixed. Catches mistakes before they reach the repository or production.
Sharing Hooks With Your Team
The .git/hooks/ directory is not committed to the repository. Each developer must set up hooks manually, which means they often don’t. Store hooks in the repository and set up a script to install them:
# Create a hooks directory in your project root
mkdir .githooks
# Move hooks there
mv .git/hooks/pre-commit .githooks/pre-commit
# Create an install script
# install-hooks.sh
#!/bin/bash
echo "Installing Git hooks..."
cp .githooks/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
echo "Hooks installed."
# Or configure Git to use the shared hooks directory
git config core.hooksPath .githooks
Add the hooks directory to the repository and document in your README that developers should run the install script after cloning.
CI/CD Basics: Automated Testing and Deployment
A CI/CD pipeline runs automated tests whenever you push code and optionally deploys automatically when tests pass. Here’s a simple GitHub Actions workflow for a PHP project:
# .github/workflows/deploy.yml
name: Test and Deploy
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.2'
extensions: pdo, pdo_mysql, curl, mbstring
- name: Install dependencies
run: composer install --no-dev --optimize-autoloader
- name: Run PHP syntax check
run: find . -name "*.php" -not -path "./vendor/*" -exec php -l {} \;
- name: Run tests
run: php artisan test
deploy:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Deploy to server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /var/www/html/your-php-project
git pull origin main
composer install --no-dev --optimize-autoloader
php artisan config:clear
php artisan cache:clear
php artisan migrate --force
echo "Deployed successfully."
Add your server credentials as repository secrets in GitHub (Settings – Secrets and Variables – Actions). The workflow then:
- Runs on every push to main and every pull request
- Checks PHP syntax on all files
- Runs your test suite
- Deploys to the server only when tests pass and the push is to main
Pull requests get tested automatically before merging. Broken code never reaches the server.
Tracking What’s Deployed
# On the server - see exactly what version is running
cd /var/www/html/your-php-project
# Show the current commit
git log -1 --oneline
# Show the current tag if on a tagged commit
git describe --tags --exact-match 2>/dev/null || git log -1 --oneline
# Show when this version was deployed
git log -1 --format="%ci"
Output:
b5c6d7e Add product search filter with pagination
v1.3.0
2026-05-03 09:00:00 +0530
Expose this in your application’s admin panel for at-a-glance deployment status:
<?php
// In a PHP admin controller
function getDeploymentInfo() {
$commit = trim(shell_exec('git log -1 --oneline'));
$version = trim(shell_exec('git describe --tags --exact-match 2>/dev/null')
?: shell_exec('git log -1 --format="%h"'));
$date = trim(shell_exec('git log -1 --format="%ci"'));
return [
'commit' => $commit,
'version' => $version,
'date' => $date,
];
}
$info = getDeploymentInfo();
echo "Version: {$info['version']}" . PHP_EOL;
echo "Commit: {$info['commit']}" . PHP_EOL;
echo "Date: {$info['date']}" . PHP_EOL;
?>
Output:
Version: v1.3.0
Commit: b5c6d7e Add product search filter with pagination
Date: 2026-05-03 09:00:00 +0530
Frequently Asked Questions
Is it safe to run git pull directly on a production server?
For small projects it works. The risks are that a merge conflict stops the pull mid-way leaving the server in an inconsistent state, and that untested code goes straight to production. The safer approach is to use tagged releases and deploy specific versions rather than pulling whatever is on main. For anything beyond a personal project, add automated tests that must pass before code reaches the server.
What’s the difference between CI and CD?
CI (Continuous Integration) is the practice of automatically running tests on every code push. It catches broken code before it merges to main. CD (Continuous Deployment or Delivery) is automatically deploying code to staging or production after tests pass. CI without CD is common – automated tests but manual deployment. CD without CI is dangerous – automatic deployment without verification.
How do I handle database migrations in a Git deployment?
Track your migration files in Git like any other code. During deployment, run migrations after pulling the latest code but before clearing the cache. In Laravel: php artisan migrate --force (the --force flag bypasses the production confirmation prompt for automated scripts). Always test migrations on a staging environment first. Write migrations that can be rolled back with php artisan migrate:rollback if something goes wrong.
My .env file isn’t in Git but the server needs it. How do I handle this?
The .env file should never be in Git. On the server, create it manually once and it stays there across deployments – git pull doesn’t overwrite files that aren’t tracked. For a new server setup, copy .env.example from the repository, fill in the production values, and save as .env. Some teams store environment variables in the server’s environment or a secrets management tool rather than a file at all.
Summary
Git-based PHP deployment gives you reproducibility, rollback capability, and automation that FTP uploads cannot:
- Tag every release – deploy specific versions not “whatever is on main”
- Check before deploying –
git diff HEAD..origin/main --name-onlyshows exactly what will change - Automate with hooks –
post-receiveon the server deploys on push,pre-commitlocally catches errors before they commit - Add a CI pipeline – tests that must pass before code reaches production
- Track what’s deployed –
git describe --tagson the server tells you exactly what version is running
This series covered the complete Git workflow for PHP developers – from initial setup through team collaboration and production deployment. For the setup and daily commands that everything builds on, start with Git for PHP developers: setup and daily workflow. For automating PHP tasks beyond Git deployment, the PHP cron job automation guide covers scheduling, logging, and debugging automated scripts.
