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.
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 -ysudo 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.pharsudo php wp-cli.phar --infosudo chmod +x wp-cli.pharsudo 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 "" deploysudo 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" 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/shellssudo 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.comsudo 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; index index.php index.html; 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; } location ~* \.(env|ini|log|sql|bak|sh)$ { deny all; } location / { try_files $uri $uri/ /index.php?$args; } location ~ \.php$ { include snippets/fastcgi-php.conf; fastcgi_pass unix:/run/php/php8.2-fpm.sock; 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 -tsudo 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/reposudo chown -R deploy:deploy /var/reposudo -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'set -euo pipefail 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 read oldrev newrev refnamebranch="${refname##refs/heads/}" if [[ "$branch" != "main" && "$branch" != "master" ]]; then echo "Skipping deploy (branch '$branch' is not set for deployment)." exit 0fi 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" mkdir -p "$SHARED/uploads" "$SHARED/env" echo "==> Linking shared folders/files"rm -rf "$new_release/wp-content/uploads"ln -s "$SHARED/uploads" "$new_release/wp-content/uploads" if [[ -f "$SHARED/env/.env" ]]; then ln -sf "$SHARED/env/.env" "$new_release/.env"fi 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 {} \; 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" fifi echo "==> Activating new release"ln -sfn "$new_release" "$CURRENT" echo "==> Reloading PHP-FPM"sudo /bin/systemctl reload php8.2-fpm || true 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:
wp-content/uploads/wp-content/upgrade/wp-content/cache/ .env*.log 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-projectgit initgit 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
git branch -M maingit 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 wp config create \ --dbname=wpdb --dbuser=wpuser --dbpass='Strong_DB_Pass_ChangeMe' --dbhost=localhost \ --dbprefix=wp_ --skip-check wp core install \ --url="https://example.com" \ --title="PerLod Production" \ --admin_user="admin" \ --admin_password="ChangeThisAdminPass!" \ --admin_email="admin@example.com" sudo chown -R deploy:www-data /var/www/example.comsudo 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:
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 0fi
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/currentsudo systemctl reload nginxsudo 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 -ysudo setfacl -R -m u:deploy:rwx -m g:www-data:rwx /var/www/example.comsudo 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:
1<?php23if (file_exists(__DIR__ . '/.env')) {4 $env = parse_ini_file(__DIR__ . '/.env');5 foreach ($env as $k => $v) $_ENV[$k] = $v;6}7 8define('DB_NAME', $_ENV['DB_NAME'] ?? 'wpdb');9define('DB_USER', $_ENV['DB_USER'] ?? 'wpuser');10define('DB_PASSWORD', $_ENV['DB_PASS'] ?? 'Strong_DB_Pass_ChangeMe');11define('DB_HOST', $_ENV['DB_HOST'] ?? 'localhost');12define('DB_CHARSET', 'utf8mb4');13define('DB_COLLATE', '');14 15$table_prefix = $_ENV['DB_PREFIX'] ?? 'wp_';16 17define('WP_DEBUG', false);18 1920define('DISALLOW_FILE_EDIT', true);21 2223if (!defined('ABSPATH')) define('ABSPATH', __DIR__ . '/');24require_once ABSPATH . 'wp-settings.php';
Here is an example Env file in /var/www/example.com/shared/env/.env:
DB_NAME=wpdbDB_USER=wpuserDB_PASS=Strong_DB_Pass_ChangeMeDB_HOST=localhostDB_PREFIX=wp_
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:
VPS Hosting for Crypto and Blockchain Apps
Use Bare Metal Servers in Edge Computing