My workflow for deploying personal projects to my VPS.
The goal is intentionally simple: Deploy with a git push, like on Heroku.
How it works:
- Push a repo to a bare Git remote on my VPS (Fedora, but you can adjust for your own distro).
- A tiny
pre-receivehook delegates to the shareddeploy.sh, which builds only pushes tomain. - Configurations live in the repo.
type="static"apps are synced to/var/www/<name>and served by host NGINX.type="proxy"apps run as Docker containers bound to127.0.0.1:<port>, with host NGINX proxying traffic to them.- Failed builds reject the push.
- No hosted PaaS, no dashboards, no per-app systemd units.
-
Install the required tools:
- Git
- NGINX
- rsync
- Docker
- yq
You will require SSH access to your VPS.
Setup compatible NGINX on your VPS using the NGINX Setup section.
-
Move
deploy.shto your VPS. -
Configure the Git templates directory:
mkdir -p ~/.config/git/template/hooks git config --global init.templateDir "~/.config/git/template"
-
Create the Git
pre-receivetemplate:cat > ~/.config/git/template/hooks/pre-receive <<'EOF' #!/bin/bash # Don't forget to change /path/to/deploy.sh exec /path/to/deploy.sh "$(basename $(pwd) .git)" "$@" EOF chmod +x ~/.config/git/template/hooks/pre-receive
-
Create a bare Git repo on the VPS that will receive your
git pushrequests:Note:
deploy.shexpects all repositories to be created in$HOME/repos. You can set theDEPLOY_REPOS_LOCATIONenv var on your VPS to change that.git init --bare /path/to/repos/repository-name.git
If the previous step was successful, you'll see
hooks/pre-receivebe automatically created in every repo you init. -
Add the VPS repository as a remote repository:
git remote add vps username@VPS_IP:/path/to/repos/repository-name.git
Remember: You need an SSH key setup in
~/.sshforusernameatVPS_IP.You can simplify this by adding a sshd config:
# ~/.ssh/config Host vps Hostname VPS_IP User username IdentityFile /path/to/key/file IdentitiesOnly yesWhich will allow you to change the remote to:
git remote add vps vps:/path/to/repos/repository-name.git
-
Now create a config file
deploy.tomlin the repository. Refer tosample.proxy.tomlorsample.static.tomlto see supported configurations. Remember that the config file should also be committed to the repo! -
git push vps localbranch:main
Notes:
-
deploy.shexpects the universal NGINX convention for configurations where every config file with a.confextension in/etc/nginx/conf.d/is loaded by default. -
It writes HTTPS NGINX configs using the first hostname in
hosts. -
Set up certs on your VPS (Follow my TLS setup if you like). The script expects certs for NGINX
server_namevalues to be in/etc/nginx/certs/server_name_dir/*. Don't forget to generate adhparam.pemfor every domain. -
It determines the
server_name_dirusing the first value from thehostssetting in your.tomlconfig file. It drops the subdomain and uses the remaining domain as the cert directory.For example, for
hosts=["www.example.com","example.com"]it expects the files:/etc/nginx/certs/example.com/fullchain.cer /etc/nginx/certs/example.com/example.com.key /etc/nginx/certs/example.com/dhparam.pem
For shashank.gg, create the cert directory and DH params:
sudo mkdir -p /etc/nginx/certs/shashank.gg
sudo openssl dhparam -out /etc/nginx/certs/shashank.gg/dhparam.pem 2048
sudo chown -R kaulsh:kaulsh /etc/nginx/certsInstall acme.sh:
curl https://get.acme.sh | sh -s email=my@example.comIssue the cert:
# Change `--dns <>` based on where your domain is hosted.
# DNS Provider Reference: https://github.com/acmesh-official/acme.sh/wiki/dnsapi
acme.sh --issue --dns dns_dgon -d shashank.gg -d *.shashank.ggInstall the certificate into the required directory:
acme.sh --install-cert -d shashank.gg -d '*.shashank.gg' \
--cert-file /etc/nginx/certs/shashank.gg/cert.cer \
--key-file /etc/nginx/certs/shashank.gg/shashank.gg.key \
--fullchain-file /etc/nginx/certs/shashank.gg/fullchain.cer \
--reloadcmd "sudo systemctl reload nginx"The wildcard cert covers shashank.gg and future subdomains like app.shashank.gg. You still need DNS records pointing each host to the VPS.
For every deployed app, deploy.sh writes an NGINX config to:
/etc/nginx/conf.d/<name>.conf
For static apps, NGINX serves /var/www/<name> and falls back to index.html, which works well for SPAs:
location / { try_files $uri $uri/ /index.html; }For proxy apps, NGINX forwards requests to the local Docker-bound port:
proxy_pass http://127.0.0.1:<port>;The script runs sudo nginx -t before reloading NGINX. If the generated config is invalid, the push is rejected.
-
Sudo Permissions: The Git hook runs as the SSH user, but deploys need to write NGINX configs, copy files to
/var/www, manage Docker containers, and reload NGINX. For a personal VPS, the simplest approach is to make your user a sudoer. A stricter setup can allow only the commands used bydeploy.sh, such asmkdir,rsync,restorecon,tee,nginx -t,systemctl reload nginx, anddocker. -
Only pushes to
maindeploy. Other branches are ignored. -
deploy.tomlis required in the repo root. Missing or invalid config rejects the push. -
hostsshould be an array. It is joined into the NGINXserver_namelist. -
build.commandis used forstaticapps.proxyapps are built with Docker usingbuild.dockerfile_path. -
build.build_locationdefaults to./for static apps. -
build.dockerfile_pathdefaults toDockerfilefor proxy apps. -
build.docker_volumesis optional. Host-side directories are created automatically withsudo mkdir -p. -
Static deploys call
restoreconon the served directory when available, which fixes SELinux file contexts on Fedora/RHEL hosts. -
The script uses
git archive "$newrev"instead ofgit checkoutbecausepre-receivehooks run before refs move and inside Git's quarantine environment.
-
If a static site returns
403 Forbiddenon Fedora/RHEL, check SELinux labels:ls -laZ /var/www/<name> sudo semanage fcontext -a -t httpd_sys_content_t "/var/www/<name>(/.*)?" sudo restorecon -Rv /var/www/<name>
-
If NGINX fails to start because it cannot read cert files, make sure the generated config points to
/etc/nginx/certs/<domain>/...and that those files exist.