Automating WordPress Deployments with VPS and Git Hooks
Deploying WordPress updates manually can be slow and cause errors, especially when managing multiple environments or frequent changes. In this guide, you’ll learn how to set up automatic WordPress deployment on VPS with Git hooks, which enables a simple and reliable continuous deployment workflow.
We will push code from the local machine to a remote Git repository on the server, then a post-receive hook will automatically check out the latest code into a timestamped release folder and update a symlink (current/) to switch versions instantly, which ensures zero downtime.
Shared files like uploads, configuration, and environment variables stay persistent across deployments.
By the end of this article, you will have a clean structure for managing staging and production environments from the same repository, which all run on an Ubuntu VPS with Nginx and PHP-FPM.
If you’re looking for a reliable environment to host and scale your WordPress projects, consider our flexible VPS hosting solutions, which offer full SSH access and powerful performance for professional deployment workflows.
Table of Contents
Preparation for WordPress Deployment in VPS with Git Hooks
The first step is to prepare your VPS by running the system update, installing the required packages, and tools.
To run the system update and upgrade, use the command below:
sudo apt update && sudo apt upgrade -y
Install Nginx and essential tools with the following command:
sudo apt install nginx git unzip curl -y
Install PHP and its required extensions for WordPress with the following command:
sudo apt install php php-fpm php-mysql php-xml php-mbstring php-curl php-zip php-gd php-intl php-bcmath -y
Then, install MariaDB and run the security script for it to set up the root password and other settings:
sudo apt install mariadb-server -y
sudo mysql_secure_installation
You must create a user database for WordPress and grant all privileges to it with the following commands:
sudo mysql -e "CREATE DATABASE wpdb DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;"
sudo mysql -e "CREATE USER 'wpuser'@'localhost' IDENTIFIED BY 'Strong_DB_Pass_ChangeMe';"
sudo mysql -e "GRANT ALL PRIVILEGES ON wpdb.* TO 'wpuser'@'localhost'; FLUSH PRIVILEGES;"
Also, you must install WP-CLI, which is a powerful command-line tool for managing WordPress. To set up WP-CLI, you can use the commands below:
sudo curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
sudo php wp-cli.phar --info
sudo chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
Once you are done with these requirements, proceed to the next steps to complete WordPress Deployment in VPS with Git Hooks.
Set Up a Dedicated User with Git-Shell
In this step, you must create a dedicated and non-privileged deploy user with the following commands:
sudo adduser --disabled-password --gecos "" deploy
sudo usermod -aG www-data deploy
Then, generate SSH keys on your local machine and upload the public key with the commands below:
sudo ssh-keygen -t ed25519 -C "localuser@deploy" # press enter to accept default
sudo ssh-copy-id deploy@YOUR_SERVER_IP
You can increase the security by restricting the user’s shell to only Git operations with the following commands:
which git-shell | sudo tee -a /etc/shells
sudo chsh -s "$(which git-shell)" deploy
Configure Web Directory Structure and Permissions for WordPress Deployment
At this point, you must create a directory layout that separates each new release from shared and persistent data. To do this, run the command below:
sudo mkdir -p /var/www/example.com/{releases,shared/{uploads,env}}
The shared/env folder will hold your environment files, and shared/uploads is your persistent WordPress uploads directory.
Also, set up the correct permissions and ownership for the files with the deploy user:
sudo chown -R deploy:www-data /var/www/example.com
sudo chmod -R 2775 /var/www/example.com
Configure Nginx Server Block for WordPress Deployment
This Nginx configuration serves as the public gateway to your application and works seamlessly with the deployment folder structure by pointing the document root to the current symlink, which allows for atomic release switches.
To create the Nginx configuration file for the WordPress deployment, run the command below:
sudo tee /etc/nginx/sites-available/example.com <<'NGINX'
server {
listen 80;
server_name example.com www.example.com;
root /var/www/example.com/current; # symlink switches atomically per release
index index.php index.html;
# Serve uploads & static quickly
location ~* \.(jpg|jpeg|gif|png|webp|svg|css|js|ico|woff2?)$ {
access_log off;
log_not_found off;
expires 7d;
try_files $uri $uri/ =404;
}
# Deny access to sensitive files
location ~* \.(env|ini|log|sql|bak|sh)$ { deny all; }
# WordPress rewrites
location / {
try_files $uri $uri/ /index.php?$args;
}
# PHP-FPM
location ~ \.php$ {
include snippets/fastcgi-php.conf;
fastcgi_pass unix:/run/php/php8.2-fpm.sock; # adjust version if needed
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
}
NGINX
Enable the Nginx server block, check for syntax errors, and restart Nginx with the following commands:
sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
Note for Apache users: The principle is the same; you must set your DocumentRoot to /var/www/example.com/current. The rest of your deployment structure and logic remains unchanged.
Automate WordPress Deployment with a Git Hook
In this step, you can automate the entire deployment process, which transforms a simple git push into a powerful and production-ready release.
Create a bare Git repository on the server that acts as a receiver for your code with the commands below:
sudo mkdir -p /var/repo
sudo chown -R deploy:deploy /var/repo
sudo -u deploy git init --bare /var/repo/wp-site.git
Next, create the post-receive hook script, which is configured to automatically deploy every push to the main branch. The following script handles creating timestamped releases, symlinking shared files, updating permissions, and cleanly switching the live site:
sudo -u deploy tee /var/repo/wp-site.git/hooks/post-receive <<'HOOK'
#!/usr/bin/env bash
set -euo pipefail
# CONFIG
APP_NAME="wp-site"
REPO_DIR="/var/repo/wp-site.git"
BASE="/var/www/example.com"
RELEASES="$BASE/releases"
CURRENT="$BASE/current"
SHARED="$BASE/shared"
KEEP_RELEASES=5
# Branch mapping: deploy only from specific branches
# You can add staging mapping if needed
read oldrev newrev refname
branch="${refname##refs/heads/}"
if [[ "$branch" != "main" && "$branch" != "master" ]]; then
echo "Skipping deploy (branch '$branch' is not set for deployment)."
exit 0
fi
timestamp="$(date +%Y%m%d%H%M%S)"
new_release="$RELEASES/$timestamp"
echo "==> Creating release: $new_release"
mkdir -p "$new_release"
echo "==> Checking out code"
GIT_WORK_TREE="$new_release" git --git-dir="$REPO_DIR" checkout -f "$branch"
# Ensure shared directories exist
mkdir -p "$SHARED/uploads" "$SHARED/env"
# Link shared content into release
echo "==> Linking shared folders/files"
rm -rf "$new_release/wp-content/uploads"
ln -s "$SHARED/uploads" "$new_release/wp-content/uploads"
# If using .env or environment files for wp-config.php
if [[ -f "$SHARED/env/.env" ]]; then
ln -sf "$SHARED/env/.env" "$new_release/.env"
fi
# Permissions
echo "==> Setting permissions"
chown -R deploy:www-data "$new_release"
find "$new_release" -type d -exec chmod 2775 {} \;
find "$new_release" -type f -exec chmod 664 {} \;
# Optional: Composer install if present
if [[ -f "$new_release/composer.json" ]]; then
echo "==> Running composer install (no-dev, optimized)"
if command -v composer >/dev/null 2>&1; then
(cd "$new_release" && composer install --no-dev --prefer-dist --no-interaction --optimize-autoloader)
else
echo "composer not found; skipping"
fi
fi
# Optional: build assets if you keep a build step (npm, etc.)
# (cd "$new_release" && npm ci && npm run build) || true
# Maintenance mode (optional; typically not needed for fast swaps)
# /usr/local/bin/wp --path="$new_release" maintenance-mode activate || true
echo "==> Activating new release"
ln -sfn "$new_release" "$CURRENT"
# Reload PHP-FPM to clear OPcache (fast & safe)
echo "==> Reloading PHP-FPM"
sudo /bin/systemctl reload php8.2-fpm || true
# /usr/local/bin/wp --path="$CURRENT" maintenance-mode deactivate || true
# Clean up old releases
echo "==> Cleaning old releases"
cd "$RELEASES"
ls -1dt */ | tail -n +$((KEEP_RELEASES+1)) | xargs -r rm -rf
echo "==> Deploy complete for branch '$branch' → $new_release"
HOOK
Finally, make the deployment script executable, so it can run automatically when you push code to the server:
sudo chmod +x /var/repo/wp-site.git/hooks/post-receive
Now you must allow the deploy user to reload PHP-FPM without entering a password, so the automated deployment script can clear PHP’s cache safely:
echo 'deploy ALL=NOPASSWD: /bin/systemctl reload php8.2-fpm' | sudo EDITOR='tee -a' visudo
Configure Git Repository for WordPress
A clean Git repository is essential for smooth deployments. This involves adding your custom code, such as themes and plugins, while excluding sensitive directories like uploads and cache.
Here is a .gitignore example in your project root:
# WordPress
wp-content/uploads/
wp-content/upgrade/
wp-content/cache/
# Sensitive
.env
*.log
# Node/Composer (if used)
node_modules/
vendor/
package-lock.json
Note: If you use Composer to manage WordPress and plugins, keep composer.lock in the repository and let the hook run composer install.
Perform First WordPress Deployment with Git Push
At this point, you must initialize your local Git repository, link it to the server, and push your code. The post-receive hook on the server will create the first release and atomically switch the current symlink, which makes your site live.
To create the repository, you can use the command below or use your existing project folder:
cd /path/to/your/wp-project
git init
git add .
git commit -m "Initial commit: WordPress site"
Add the remote to the server’s bare repository:
git remote add production deploy@YOUR_SERVER_IP:/var/repo/wp-site.git
Push the main code:
git branch -M main
git push production main
Deploy Future Changes: For all subsequent updates, this single command is all you need. The entire deployment process is now automated.
Final WordPress Configuration Setup
After your first successful deployment, you need to complete the WordPress setup. For a fresh site, use the following WP-CLI commands to generate the wp-config.php file and install WordPress core:
Note: If you are migrating an existing site, you can skip the core installation.
cd /var/www/example.com/current
# Create wp-config if you keep an example in repo; otherwise use WP-CLI
wp config create \
--dbname=wpdb --dbuser=wpuser --dbpass='Strong_DB_Pass_ChangeMe' --dbhost=localhost \
--dbprefix=wp_ --skip-check
# Install core
wp core install \
--url="https://example.com" \
--title="PerLod Production" \
--admin_user="admin" \
--admin_password="ChangeThisAdminPass!" \
--admin_email="[email protected]"
# Set file permissions (once more, just to be sure)
sudo chown -R deploy:www-data /var/www/example.com
sudo find /var/www/example.com -type d -exec chmod 2775 {} \;
sudo find /var/www/example.com -type f -exec chmod 664 {} \;
Set Up a Staging Environment from the Same Repository (Optional)
You can extend this deployment system to support a separate staging environment from the same repository. By modifying the post-receive hook to deploy different branches to different server paths, you can push to staging for testing and main for production:
# in post-receive, replace the mapping with:
if [[ "$branch" == "main" ]]; then
BASE="/var/www/example.com"
elif [[ "$branch" == "staging" ]]; then
BASE="/var/www/staging.example.com"
else
echo "Skipping deploy for branch '$branch'"
exit 0
fi
# The rest of the script stays the same, using $BASE
Just as you did for production, create a new Nginx virtual host for staging.example.com that uses the staging site’s current symlink as its document root.
Instant WordPress Deployment Rollback to a Previous Version
To quickly rollback a problematic deployment, you can manually point the current symlink to an older release directory and reload the web services. In this way, your shared uploads and environment files remain unaffected.
List the releases with the following command:
ls -1dt /var/www/example.com/releases/*
Then, symlink the current to a previous release:
sudo ln -sfn /var/www/example.com/releases/20251108112530 /var/www/example.com/current
sudo systemctl reload nginx
sudo systemctl reload php8.2-fpm
WordPress Write Access Setup
To ensure WordPress can function properly, you must set the correct ownership and permissions that allow file uploads via the admin while keeping code secure.
To ensure the deploy user owns the code, run the command below:
sudo chown -R deploy:www-data /var/www/example.com
Also, you can use ACLs alternative method for this:
sudo apt install acl -y
sudo setfacl -R -m u:deploy:rwx -m g:www-data:rwx /var/www/example.com
sudo setfacl -dR -m u:deploy:rwx -m g:www-data:rwx /var/www/example.com
Environment-Based WordPress Configuration
Instead of writing your database details directly in the wp-config.php file, you can use an environment variable that loads them from a .env file that’s stored safely outside the web folder. This will keep your sensitive information secure and make it easy to use different settings for staging and production.
This wp-config.php file lives in your Git repository and gets deployed with every release:
<?php
// Load .env if present
if (file_exists(__DIR__ . '/.env')) {
$env = parse_ini_file(__DIR__ . '/.env');
foreach ($env as $k => $v) $_ENV[$k] = $v;
}
define('DB_NAME', $_ENV['DB_NAME'] ?? 'wpdb');
define('DB_USER', $_ENV['DB_USER'] ?? 'wpuser');
define('DB_PASSWORD', $_ENV['DB_PASS'] ?? 'Strong_DB_Pass_ChangeMe');
define('DB_HOST', $_ENV['DB_HOST'] ?? 'localhost');
define('DB_CHARSET', 'utf8mb4');
define('DB_COLLATE', '');
$table_prefix = $_ENV['DB_PREFIX'] ?? 'wp_';
define('WP_DEBUG', false);
// Harden file edit in admin
define('DISALLOW_FILE_EDIT', true);
// Standard WordPress bootstrap
if (!defined('ABSPATH')) define('ABSPATH', __DIR__ . '/');
require_once ABSPATH . 'wp-settings.php';
Here is an example Env file in /var/www/example.com/shared/env/.env:
DB_NAME=wpdb
DB_USER=wpuser
DB_PASS=Strong_DB_Pass_ChangeMe
DB_HOST=localhost
DB_PREFIX=wp_
FAQs
What are Git hooks, and why use them for WordPress deployment?
Git hooks are scripts that run automatically when certain Git events occur. Using a post-receive hook on your VPS lets you automatically deploy code when you push to your remote repository.
Do I need to reinstall WordPress for every deployment?
No. WordPress core files are version-controlled, and the database remains intact. The deployment only updates files, not your content or settings.
What is a bare Git repository, and why do we use it on the server?
A bare repository doesn’t have a working directory. It only stores Git data and is designed to serve as a remote endpoint for pushes.
Your deployment hook checks out a working copy from this bare repo into your live directory.
Conclusion
Setting up WordPress Deployment in VPS with Git Hooks makes your workflow faster, safer, and more modern. With this system, every time you push changes to your Git repository, your site updates automatically with no manual uploads or version mix-ups.
Whether you’re using PerLod Hosting or any VPS with root access, this step-by-step guide helps you implement a professional deployment workflow.
We hope you enjoy this guide. Subscribe to our X and Facebook channels to get the latest WordPress and hosting articles.
For further reading: