Automated Git Deploy on VPS: CI/CD Beginner Guide

Tired of uploading files via FTP every time you make a change, or SSH-ing into your server to manually pull updates? If you already have a VPS, you can set up automatic Git deployment so that every git push instantly deploys your latest code to the server.

This guide walks you through creating a Git Bare Repository + post-receive Hook on Ubuntu VPS from scratch. It works with HTML, PHP, Node.js, or Python projects and serves as the foundation for modern CI/CD workflows.

Prerequisites: Ubuntu 20.04/22.04 VPS with Git installed, SSH access, Nginx or Apache running, and Git installed on your local machine.

How Does Git Deploy Work?

The idea is to create a Bare Repository on the VPS to receive pushes from your local machine, then use a Git Hook named post-receive to automatically check out the latest code to the web root every time a push arrives.

StepDescription
1. Bare RepoCreate a Git repository on VPS (no working tree) — receives pushes only
2. post-receiveShell script that runs automatically after each push — checks out files to web root
3. RemoteAdd the VPS as a Git remote on your local machine
4. git pushPush code to VPS — hook fires — website updates automatically

Step 1 — Prepare VPS Folder Structure

SSH into your VPS and create two directories: the Bare Repository and the Web Root:

# Bare repository — receives Git pushes
mkdir -p /home/deploy/repos/mysite.git

# Web root — Nginx/Apache serves files from here
mkdir -p /var/www/mysite

Why two directories? A bare repository stores only Git objects (no project files). The web root holds the actual files served by your web server. This separation is a best practice — it keeps Git internals out of the public web directory.

Step 2 — Initialize Bare Repository

cd /home/deploy/repos/mysite.git
git init --bare

You should see: Initialized empty Git repository in /home/deploy/repos/mysite.git/. The directory will contain HEAD, config, objects/, refs/ — no project files.

Step 3 — Create the post-receive Hook

Hooks are scripts Git runs automatically when events occur. The post-receive hook fires after a push is accepted:

nano /home/deploy/repos/mysite.git/hooks/post-receive

Paste this content:

#!/bin/bash
GIT_WORK_TREE=/var/www/mysite GIT_DIR=/home/deploy/repos/mysite.git git checkout -f main
echo "✅ Deploy successful → /var/www/mysite"

Save and make it executable:

chmod +x /home/deploy/repos/mysite.git/hooks/post-receive

Important: If your default branch is master instead of main, update the git checkout line accordingly.

Step 4 — Set Web Root Permissions

Ensure the web server user has read access to the web root (typically www-data for Nginx/Apache on Ubuntu):

chown -R www-data:www-data /var/www/mysite
chmod -R 755 /var/www/mysite

If you deploy as a deploy user instead of root, add that user to the www-data group:

usermod -aG www-data deploy

Step 5 — Add VPS as a Git Remote (Local Machine)

On your local machine (not the VPS), navigate to your project folder and add the VPS as a remote named production:

# If you don't have a local repo yet
git init
git add .
git commit -m "Initial commit"

# Add VPS as remote (replace YOUR_VPS_IP)
git remote add production ssh://root@YOUR_VPS_IP/home/deploy/repos/mysite.git

Use SSH Keys: If you have SSH key authentication configured, pushes won't require a password. See our guide on Connecting to VPS via SSH for setup instructions.

Step 6 — First Deploy

Push your code to the VPS for the first time:

git push production main

If everything is configured correctly, you'll see output like this:

Counting objects: 5, done.
Writing objects: 100% (5/5), 512 bytes | 512.00 KiB/s, done.
remote: ✅ Deploy successful → /var/www/mysite
To ssh://root@YOUR_VPS_IP/home/deploy/repos/mysite.git
 * [new branch]      main -> main

Verify the files are in the web root:

ls -la /var/www/mysite

Step 7 — Configure Nginx Virtual Host

If you haven't set up a virtual host yet, create one pointing to the web root:

nano /etc/nginx/sites-available/mysite
server {
    listen 80;
    server_name yourdomain.com www.yourdomain.com;
    root /var/www/mysite;
    index index.html index.php;

    location / {
        try_files $uri $uri/ =404;
    }
}
# Enable the config
ln -s /etc/nginx/sites-available/mysite /etc/nginx/sites-enabled/
nginx -t && systemctl reload nginx

Step 8 — Daily Workflow Going Forward

Once set up, deploying from now on is just three commands:

# Make changes, commit, and push
git add .
git commit -m "Fix navigation header"
git push production main

The hook checks out the latest files to the web root instantly — no FTP, no manual SSH.

Security Hardening for Your Git Deploy Setup

A misconfigured Git deploy can expose your server to unauthorized access. Before going live with any production deployment, apply these security measures:

1. Disable Password Authentication on SSH

SSH key authentication is cryptographically stronger than passwords and immune to brute-force attacks. Disable password login for all users:

# Edit /etc/ssh/sshd_config
PasswordAuthentication no
PubkeyAuthentication yes
# Restart SSH service to apply changes
systemctl restart sshd

2. Create a Dedicated Deploy User

Never use root for automated deployments. Create a deploy user with permissions limited to the bare repository and web root:

# Create deploy user with no shell access
adduser --disabled-login --gecos "" deploy
# Grant ownership of the bare repo and web root
chown -R deploy:deploy /home/deploy/repos/
chown -R deploy:www-data /var/www/mysite
chmod -R 775 /var/www/mysite

3. Restrict SSH Key to Git Commands Only

Add a command= restriction to the deploy key in authorized_keys so that even if the key is compromised, it can only run Git operations:

# In /home/deploy/.ssh/authorized_keys
command="git-shell -c \"$SSH_ORIGINAL_COMMAND\"",no-port-forwarding,no-X11-forwarding ssh-rsa AAAA... your-deploy-key

Security checklist before production: Disable PasswordAuthentication, use SSH keys only, configure UFW firewall, use a dedicated deploy user, and review SSH logs regularly with journalctl -u sshd.

Managing Environment Variables and Secrets in Hooks

Modern applications rely on environment variables for database credentials, API keys, and other secrets that should never be committed to version control. Here are two reliable approaches for managing secrets on the server:

Approach 1: Source a Secrets File from Outside the Web Root

Create an .env file in a directory the web server cannot read, and source it in the hook:

#!/bin/bash
GIT_WORK_TREE=/var/www/mysite GIT_DIR=/home/deploy/repos/mysite.git git checkout -f main
# Load secrets stored outside the web root
source /home/deploy/secrets/mysite.env
# Run database migrations (example for Laravel)
cd /var/www/mysite
php artisan migrate --force
echo "✅ Deployed with secrets loaded"

Set strict permissions on the secrets directory:

mkdir -p /home/deploy/secrets
chmod 700 /home/deploy/secrets
nano /home/deploy/secrets/mysite.env
# Example .env content
export DB_HOST=localhost
export DB_NAME=mysite_db
export DB_PASS=your_secure_password
export APP_KEY=base64:xxxxxxxxxxxxx

Approach 2: Symlink .env After Checkout

If your framework reads .env from the project root, create a symlink in the hook:

#!/bin/bash
GIT_WORK_TREE=/var/www/mysite GIT_DIR=/home/deploy/repos/mysite.git git checkout -f main
# Symlink persistent .env from outside the repo
ln -sf /home/deploy/secrets/mysite.env /var/www/mysite/.env
echo "✅ Deployed and .env linked"

Never commit secrets to Git. Even in a private repository, once a secret appears in commit history it must be rotated immediately — history cannot be easily erased from all clones. If a secret is accidentally committed, revoke it and generate a new one before anything else.

Comparing Git Deploy to Other Deployment Methods

Understanding where Git Deploy fits in the ecosystem helps you choose the right tool for each project stage:

Method Complexity Best For Limitation
Git Bare + Hook Low–Medium Single VPS, small team No automatic rollback
GitHub Actions Medium Test-gated deployments Requires GitHub, usage limits
FTP Manual Very Low Shared hosting, tiny sites Slow, prone to human error
rsync + SSH Low Static sites, no history needed No version control
Docker + Registry High Microservices, multi-server Requires Docker expertise

For developers with a single VPS running small-to-medium projects, Git Bare + Hook is the fastest path to automated deployment — set up in under 30 minutes, zero external dependencies, and easily extended into a full CI/CD pipeline later.

Push to GitHub and VPS Simultaneously

To push to GitHub and your VPS at the same time, you can add multiple push URLs to a single remote:

# Add GitHub as origin
git remote add origin https://github.com/youruser/mysite.git

# Or configure multiple push URLs for one remote
git remote set-url --add --push origin https://github.com/youruser/mysite.git
git remote set-url --add --push origin ssh://root@YOUR_VPS_IP/home/deploy/repos/mysite.git

# One push goes to both GitHub and VPS
git push origin main

Add a Build Step in the Hook (Node.js / PHP)

For projects that need a build step, add it directly to the hook:

Node.js Example

#!/bin/bash
GIT_WORK_TREE=/var/www/mysite GIT_DIR=/home/deploy/repos/mysite.git git checkout -f main
cd /var/www/mysite
npm install --production
pm2 restart mysite || pm2 start app.js --name mysite
echo "✅ Deployed and PM2 restarted"

PHP Composer Example

#!/bin/bash
GIT_WORK_TREE=/var/www/mysite GIT_DIR=/home/deploy/repos/mysite.git git checkout -f main
cd /var/www/mysite
composer install --no-dev --optimize-autoloader
echo "✅ Deployed and Composer installed"

Troubleshooting Common Issues

Push succeeded but no files in web root

Verify the branch name matches between your local machine and the hook (main vs master). Check the hook file is correct:

cat /home/deploy/repos/mysite.git/hooks/post-receive

Permission denied when pushing

Verify your SSH key is correctly configured and the user has write access to the bare repository:

ls -la /home/deploy/repos/

Website shows 403 after deploy

Fix web root permissions:

chown -R www-data:www-data /var/www/mysite
find /var/www/mysite -type d -exec chmod 755 {} \;
find /var/www/mysite -type f -exec chmod 644 {} \;

Next steps: Once comfortable with Git Deploy, level up to a full CI/CD pipeline using GitHub Actions or GitLab CI — automatically run tests before every deploy to ensure only passing code reaches production.

Need a VPS for Git Deployment?

AsiaGB offers VPS in Thailand and Singapore with full root access starting at ฿500/month.

View VPS Plans →