How To Set Up CI/CD for a Static Site With GitHub Actions and Nginx
Goal
In this tutorial, you will create a least-privilege deploy user and a purpose-scoped SSH key on your server, store the private key as an encrypted GitHub Actions secret, and write a workflow that builds your static site and rsyncs the output to your Nginx document root on every push to main.
Prerequisites
- A Debian or Ubuntu server with Nginx already installed and serving a domain from a directory (e.g. /var/www/example.com/html) — adduser as shown here is Debian/Ubuntu-specific
- Sudo access on that server
- A GitHub repository containing your site's source code
- Basic familiarity with SSH key pairs and the command line
- Node.js/npm or your static site generator's toolchain, if your site has a build step
Let an AI agent do this for you
Copy a ready-made prompt for an AI coding assistant with terminal access to your server (Claude Code, Cursor, or similar) — it can carry out the steps below for you. Review what it plans to run before it executes anything.
Manually building a static site and uploading the output with scp or an FTP client works until you forget a step, or forget to do it at all after a late-night edit. This tutorial wires up a GitHub Actions workflow that builds your site on every push to main and deploys the result straight to an Nginx document root over SSH, using a deploy key that can do nothing else on the server.
The pipeline has two halves: a purpose-created Linux user and SSH key pair on the server that can write to exactly one directory, and a workflow file in the repository that builds the site on GitHub's own runners and rsyncs the output to that directory. Nothing touches your personal SSH key, and nothing needs manual upload again.
Step 1 — Create a Dedicated Deploy User on the Server
Deploying as root or your own login account works, but it means a leaked CI secret compromises far more than the one directory the site lives in. Create a low-privilege system user whose only job is writing into the site's document root.
sudo adduser --disabled-password deploy-bot
sudo mkdir -p /var/www/example.com/html
sudo chown -R deploy-bot:deploy-bot /var/www/example.com/html--disabled-password skips setting a Unix password entirely, so the account can only be reached by SSH key, never by password guessing. Adjust the path to whatever directory your existing Nginx server block already points its root at.
“Do not add deploy-bot to the sudo group and do not reuse an existing personal or admin account for this. It should only ever be able to write into the one document root it owns.”
Step 2 — Generate a Deploy-Specific SSH Key Pair
Run this on your own workstation, not on the server. GitHub Actions needs a key it can use non-interactively, so it can't have a passphrase — an empty passphrase is standard practice for CI keys as long as the key is scoped to nothing else and stored only in GitHub's encrypted secret store.
ssh-keygen -t ed25519 -f deploy_key -N ""This produces two files in the current directory: deploy_key (the private key) and deploy_key.pub (the public key). Lock down the private key immediately, before it goes anywhere else.
chmod 600 deploy_keyStep 3 — Install the Public Key on the Server
adduser creates a home directory but not a .ssh subdirectory, so create it yourself with the exact permissions sshd requires. This is not cosmetic: sshd silently refuses to use a key if the owning directory or authorized_keys file is writable by anyone other than the owner, with no error surfaced to the connecting client — it just falls through to the next auth method and fails.
Tricknowtech VPS Hosting
Dedicated KVM resources and full root access — deployed in under 60 seconds, no ticket required.
Ready to try it yourself?
Create a free account and follow along.