Ahosting Logo
Knowledge Base

How to Deploy a Site with Git on a VPS

The deployment mistake with the largest consequenceChecking out into public_html· puts the .git directory inside the web root· the entire history becomes downloadable· including credentials removed in a later commitDeploy from outside it· the repository lives outside the web root· files are copied or symlinked into place· and the switch can be atomicWhat stays out of the repository entirelyConfiguration with credentials, uploaded media, and anything generated. Those belong beside thecheckout, not in it.

Deploying with Git means the server receives your code from a repository rather than from an FTP client. What you gain is a deployment that is repeatable, reviewable and revertible: the last one being why it matters at two in the morning.

Two ways to do it

Pull. The server has a checkout and runs git pull when you tell it to. Simple, and it means the server needs read access to the repository.

Push. You push to a bare repository on the server, and a hook checks the code out into the web directory. Nothing outbound is needed, and it works when the server cannot reach your Git host.

Pull is the usual choice because it fits how hosted repositories work. Push suits a server that must not make outbound connections.

Do not make the repository the web root

The mistake with the largest consequence.

Checking out directly into public_html puts .git inside the web root. That directory contains your entire history, and on a server that does not block it, anyone can download it, including any credential ever committed, even one you removed later.

Check out above the web root and either symlink the public directory or configure the web server to serve a subdirectory of the checkout.

If you have already done it the other way, assume the history is public and rotate anything sensitive in it. Understanding file permissions goes into why placement beats permissions here.

Use a deploy key

Give the server its own key with read-only access to the repository, rather than reusing a personal key.

It cannot have a passphrase, since nothing is there to type one, so treat it as more sensitive rather than less. Read-only means a compromised server cannot rewrite your repository.

Keep it out of the checkout itself. A deploy key committed to the repository it deploys is a genuinely common mistake. Managing SSH keys walks through generating one.

What does not belong in the repository

Configuration holding credentials. Uploaded files. Anything generated during a build.

Those belong in environment variables or a configuration file on the server, excluded by .gitignore and never committed, because removing a secret later does not remove it from the history, and the history is what gets cloned.

For WordPress specifically: track the theme and any custom plugins, and exclude wp-config.php and wp-content/uploads. Uploads are user data rather than code, and a repository containing them grows without limit.

A deployment is more than a pull

The step people discover after the first one that half-worked.

A real deployment usually needs to install dependencies, run database migrations, clear caches and restart a long-running process, and doing those by hand means one is eventually forgotten.

Put them in a script that runs after the pull, so the deployment is one command rather than five remembered ones. On a long-running application, the restart is not optional: the code changes and the running process keeps serving the old version. There is more on that specific surprise in running Node.js and Python applications.

The gap while files are changing

Pulling into a live directory means visitors during that second see a mixture of old and new files, which can produce errors for anyone unlucky enough to arrive mid-pull.

For a small site that is usually acceptable. For anything busier, deploy into a new directory and switch a symlink when it is ready. The switch is atomic, so no request sees a half-updated site.

That arrangement also gives you the rollback: the previous release is still on disk, and reverting is pointing the symlink back.

Automating it

A webhook from your Git host can trigger the deployment on push, which removes the manual step.

Two conditions before doing that. The endpoint must verify the request came from your Git host, using the signature they provide. An unauthenticated deploy endpoint lets anyone trigger a deployment. And it must deploy from a specific branch, so work in progress does not reach production.

Deploying every push to the main branch is fine when the main branch is protected. It is not fine when everyone pushes to it directly.

Keep the ability to go back

The reason for all of this.

Know the command that reverts to the previous release, and have run it once deliberately so it is familiar rather than theoretical.

And remember what a code rollback does not undo: database migrations have already run, and reverting the code against a migrated database can be worse than the bad deployment. Take a database backup before any deployment that migrates. Backing up and restoring your VPS explains having one.

Check it after, not just that it ran

A deployment that completed is not a deployment that worked.

Load a real page, check the version actually changed, and read the error log instead of assuming silence means success: a fatal error on a path nobody has visited yet is waiting there, and the first visitor finds it. Managing logs walks through where to look.

Deploying the code is half of it; keeping the process running across reboots and crashes is the other. There is more on that half in How to Run an Application as a systemd Service.

Confirm what actually changed after a deployment

A deployment that reports success has copied files, which is not the same as the site running the new version.

git -C ~/app rev-parse --short HEAD
curl -s https://example.com/version.txt 2>/dev/null
ls -la --time-style=+%F' '%H:%M ~/public_html/index.php

Publishing the running version somewhere the site can report it removes the guesswork entirely, and it costs one file written during the deployment.

Caching is the usual reason a deployment appears not to have taken effect. The files are new and the pages being served are stored copies, which is a different problem from a failed deployment and is fixed differently.

Keep the previous version reachable

Going back is the part that is arranged after it is needed unless it is arranged in advance.

git -C ~/app log --oneline -5
git -C ~/app tag -l | tail -5

Tagging each release means reverting is naming a tag rather than reconstructing which state was working.

Where the deployment runs database changes, going back is not symmetrical. A schema change applied forward is not undone by checking out the previous code, and that is the case worth planning for specifically rather than assuming a revert is always available.