TutorialsAugust 4, 20264 min read

How to Set Up Git-Based Deploys on a VPS

Deploy by pushing to your server: set up a bare Git repo with a post-receive hook so 'git push' builds and reloads your app.

NBy Nxeon

Uploading files by FTP is slow and error-prone. A much cleaner workflow is to deploy by pushing to your server with Git: git push production main checks out the new code and runs a build-and-restart hook automatically. This guide sets up a bare repository with a post-receive hook on Ubuntu 24.04.

How Git deploys work

You create a bare repository on the server (a repo with no working files, just Git data) and add it as a remote on your local machine. When you push, a post-receive hook — a shell script Git runs after accepting a push — checks the code out into your app directory and restarts the service. No FTP, no manual pull.

Install Git and prepare directories

sudo apt update
sudo apt install git -y
mkdir -p ~/repos/myapp.git ~/apps/myapp
cd ~/repos/myapp.git
git init --bare

~/repos/myapp.git is the bare repo you push to; ~/apps/myapp is where the live code is checked out and served from.

Write the post-receive hook

Create ~/repos/myapp.git/hooks/post-receive:

#!/bin/bash
set -e
TARGET="/home/youruser/apps/myapp"
BRANCH="main"

while read oldrev newrev ref; do
  if [ "$ref" = "refs/heads/$BRANCH" ]; then
    echo "Deploying $BRANCH to $TARGET"
    git --work-tree="$TARGET" --git-dir="$HOME/repos/myapp.git" checkout -f "$BRANCH"
    cd "$TARGET"
    npm ci --omit=dev
    npm run build
    pm2 reload myapp || pm2 start npm --name myapp -- start
    echo "Deploy complete"
  fi
done

Make it executable:

chmod +x ~/repos/myapp.git/hooks/post-receive

The --work-tree trick checks the pushed branch out into your app directory. Adapt the build/restart lines to your stack — a Python app might run pip install -r requirements.txt and sudo systemctl restart myapp instead. This pairs perfectly with PM2 or a systemd service.

The Nxeon game-server control panel — live console, player slots, and TPS
The Nxeon game-server control panel — live console, player slots, and TPS

Add the remote locally and push

On your development machine, add the server as a remote (using SSH — set up keys first with our SSH keys guide):

git remote add production youruser@YOUR_SERVER_IP:repos/myapp.git
git push production main

Watch the hook output stream back in your terminal as it deploys. From now on, every git push production main ships your code.

Handling secrets and env files

Keep your .env on the server only — never commit it. Because the hook checks out with checkout -f, files that are gitignored and live in the app directory persist across deploys, so your environment file stays put. Store it once in ~/apps/myapp/.env and your app reads it each restart.

Zero-downtime reloads

Using pm2 reload (as in the hook) cycles workers gracefully so users never see an error mid-deploy. For a systemd app, systemctl restart is near-instant; if you need true zero downtime there, run two instances behind Nginx and reload them one at a time — see running staging and production on one VPS.

Fail safely when a build breaks

The hook uses set -e, so if npm ci or the build fails, the script stops before restarting the app — leaving the last working version running. That is exactly the behaviour you want: a broken push should never take your site down. To make it safer still, build into a temporary directory and only swap it into place once the build succeeds, so a half-finished checkout is never served.

Restrict who can push

Because a push runs code on your server, treat the bare repo like production access. Deploy over SSH keys only, never passwords, and consider a dedicated deploy user with just enough permission to write the app directory and restart its service via a narrow sudoers rule. Combine this with the wider Linux VPS hardening checklist. If several people deploy, their individual SSH keys in ~/.ssh/authorized_keys give you a clear audit trail of who shipped what.

FAQ

Git push deploys vs a CI/CD pipeline — which should I use?

Push-to-deploy is perfect for solo developers and small projects: no external services, instant feedback. A full CI/CD pipeline (GitHub Actions, GitLab CI) adds automated tests and multi-stage deploys for teams. You can even have CI push to this same bare repo.

Can I deploy from GitHub instead of pushing directly?

Yes — either add your VPS as a second remote and push to both, or have a GitHub Action SSH in and pull. The bare-repo hook approach here needs no third party and works offline.

Why does my hook not run?

The file must be named exactly post-receive, live in the bare repo's hooks/ directory, and be executable (chmod +x). Test by pushing and watching the output; errors print to your local terminal.

How do I roll back a bad deploy?

Because it is Git, roll back by pushing an earlier commit: git push production <good-commit>:main --force, which re-triggers the hook to check out the older code. Keeping releases in Git makes rollbacks trivial.

Nxeon VPS hosting for developers gives you full root and SSH access to set up push-to-deploy in minutes on fast NVMe — with free migration help to bring an existing project across.

#git#deployment#vps#ci#devops#seobatch

Deploy your first server in under a minute

Creating an account is free and takes no card details. You pay when you deploy — choose a billing term and pay from your wallet or by card at checkout.