The Problem: The Nightmare of “Manual Uploads”
You’ve just finished fixing a “critical” bug at 2 AM. Instead of going to sleep, you have to open FileZilla, find the right folder to drag and drop files, or SSH into the server to type git pull and restart the service. These repetitive tasks are not only boring but also prone to errors like overwriting the wrong files or forgetting to restart the server.
Back when I was freelancing, setting up GitHub Actions or Jenkins for a simple landing page or a small Telegram bot often took more time than writing the code itself. I needed a solution: Push code and be done. That’s when I realized Git Push-to-Deploy with a Bare Repository was the perfect lifesaver.
Why Use a Bare Repository?
When you run git init on a regular project, Git creates a hidden .git folder alongside the Working Directory (where your editable code lives). However, Git prevents pushing directly into an active Working Directory to avoid data conflicts.
Bare Repository solves this problem by completely removing the Working Directory. It only acts as a “storage” for history and objects. We will use this repository as a transit station, then use a Hook to automatically push the code to the actual application directory.
Step 1: Setting up the Server Environment
Suppose you have an Ubuntu VPS and want to deploy a Node.js application at /var/www/my-app. First, create the application folder and the Repository folder.
# Create the application directory
sudo mkdir -p /var/www/my-app
sudo chown $USER:$USER /var/www/my-app
# Create the Bare Repository
mkdir -p ~/repos/my-app.git
cd ~/repos/my-app.git
git init --bare
After running the commands above, the my-app.git directory will only contain configuration files like hooks, info… You won’t see the source code here—don’t worry, that’s the nature of a Bare Repo.
Step 2: Activating the Post-receive Hook
Hooks are scripts that run automatically when Git events occur. The post-receive script will be triggered immediately after the server receives all data from your push command.
Create the script file:
nano ~/repos/my-app.git/hooks/post-receive
Paste the following script content:
#!/bin/bash
TARGET="/var/www/my-app"
GIT_DIR="$HOME/repos/my-app.git"
while read oldrev newrev ref
do
# Only deploy when pushing to the main branch
if [[ $ref =~ .*/main$ ]];
then
echo "Main branch received. Deploying..."
git --work-tree=$TARGET --git-dir=$GIT_DIR checkout -f main
# Perform additional tasks
cd $TARGET
npm install --production
pm2 restart my-app || pm2 start app.js --name my-app
echo "Deployment complete!"
else
echo "Branch $ref does not support auto-deployment."
fi
done
Grant execution permissions so the script can run:
chmod +x ~/repos/my-app.git/hooks/post-receive
Step 3: Connecting from Your Local Machine
On your local machine, add a new “remote” pointing to the VPS address. This only needs to be done once per project.
# Replace 'user' and 'ip' with your VPS information
git remote add deploy [email protected]:/home/user/repos/my-app.git
Step 4: Enjoy the New Workflow
Now, whenever you update your code, just type:
git push deploy main
The entire process takes less than 5 seconds. The terminal will display logs from the server, letting you know exactly if npm install ran or if pm2 restarted. You save at least 15-20 minutes a day compared to the manual method.
Practical Tips and Considerations
Through many client projects, I’ve gathered 3 important notes to avoid “silly” mistakes:
- Write Permissions: Your SSH user must have write access to
/var/www/my-app. If you encounter aPermission deniederror, double-check thechowncommand from Step 1. - Cleaning up junk files: The
checkout -fcommand overwrites old files but doesn’t delete files you’ve removed in Git. To keep the server clean, addgit --work-tree=$TARGET --git-dir=$GIT_DIR clean -fdto your hook script. - Security with SSH Keys: Use SSH Keys instead of typing a password every time you push. It’s both faster and much more secure.
Conclusion
This method is perfect for freelancers or small teams that need speed and simplicity. It doesn’t replace massive CI/CD systems like GitHub Actions for large projects, but it wins hands down in terms of simplicity and resource efficiency. Instead of spending an entire afternoon writing complex YAML files, you only need 5 minutes to free yourself from tedious tasks.

