
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.
| Step | Description |
|---|---|
| 1. Bare Repo | Create a Git repository on VPS (no working tree) — receives pushes only |
| 2. post-receive | Shell script that runs automatically after each push — checks out files to web root |
| 3. Remote | Add the VPS as a Git remote on your local machine |
| 4. git push | Push 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 nginxStep 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 mainThe 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-keySecurity 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:xxxxxxxxxxxxxApproach 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 →