Deploy with Git
Edited 16 August 2026. The troubleshooting
section below used to fix permission problems with
chown -R www-data:www-data /var/www/html. That is bad advice, and
I had been running exactly that on my own server, so this is a correction
rather than a tidy-up. Making the web root writable by the user nginx runs as
means any hole in nginx can rewrite the site nginx is serving. The web server
only ever needs to read those files. That section has been rewritten.
There's an entire industry built around deploying websites. CI/CD pipelines, build servers, deployment platforms, GitHub Actions, Vercel, Netlify, Render. Some of them are genuinely useful. Some of them exist to sell you a solution to a problem you could solve with a shell script and a git hook.
For a static site, the deployment problem is: "I changed a file locally, and I want it to appear on my server." That's it. Git already knows how to move files between machines. You just need to tell it to put them in the right place when they arrive.
The setup
You need git on your server (you probably already have it):
sudo apt update
sudo apt install git
Create a bare repository. This is the thing that receives your pushes:
sudo mkdir -p /var/repo/website.git
cd /var/repo/website.git
sudo git init --bare
Now the magic part. Create a post-receive hook. This is a script that runs every time the repo receives a push:
nano /var/repo/website.git/hooks/post-receive
Put this in it:
#!/bin/bash
GIT_WORK_TREE=/var/www/html git checkout -f main
Make it executable:
chmod +x /var/repo/website.git/hooks/post-receive
That's the entire deployment pipeline. Two lines of bash. When you push to this
repo, git checks out the latest version of main into your web root.
Nginx serves it. Website updated.
Pushing to it
On your local machine, add the server as a remote:
git remote add production ssh://your-username@your-server-ip:/var/repo/website.git
Then whenever you want to deploy:
git push production main
Done. No build step. No waiting for a pipeline. No "your free tier has run out of build minutes." Just your files, on your server, updated the moment you push.
If things don't update
It's almost always permissions. The web directory should be owned by whatever user does the deploying, with the web server's user only able to read it:
sudo chown -R deploy:www-data /var/www/html
sudo find /var/www/html -type d -exec chmod 755 {} +
sudo find /var/www/html -type f -exec chmod 644 {} +
Substitute whichever account runs your hook for deploy. The group is
www-data so nginx can read the files; the owner is the deploying user,
so nginx cannot write them.
This article used to say chown -R www-data:www-data, which is what most
tutorials say and what I was running myself until August 2026. It works, and that is
exactly the problem: it hands write access to the one process on the box that is
exposed to the internet. A remote code execution bug in nginx, or in anything else
running as www-data, stops being a temporary compromise and becomes a
permanent one, because the attacker can rewrite the files being served and every
visitor afterwards gets their version. Serving a static site never needs that. Check
yours with:
sudo -u www-data touch /var/www/html/test
# touch: cannot touch '/var/www/html/test': Permission denied
That error is the correct outcome. If the file gets created instead, your web server can rewrite your website.
And make sure the hook is actually executable. I've forgotten this step more times than I'd like to admit.
Why not use GitHub Actions
You can. This site actually uses GitHub Actions now for convenience. But the post-receive hook version has zero dependencies on any third-party service. No GitHub outage, no Actions quota, no YAML files that take longer to debug than the actual code. For a simple static site, a shell script is plenty. Sometimes the simplest tool is the right one.