git.guide

Deploying without publishing the repository

A block rule is a guard against a mistake. This page is about not making the mistake. If the repository never reaches the web server, there is nothing to block, nothing to forget, and nothing to leak the next time somebody rebuilds the box.

The habit to leave behind

Deploying by pulling is convenient and it puts the archive in the web root.

It is easy to see why it caught on. Log in, run a pull, the site is updated, and rolling back is one command. No build step, no tooling, nothing to learn.

What comes with it is a working copy on the server, which means the full history on the server, sitting inside the directory the web server has been told to serve. It also means the server needs credentials that can reach your repository, which is a second thing worth not having on a web server.

The three approaches below all keep the convenience and drop the archive.

1. Export instead of copying

The simplest change, and the one that needs no new tooling. Git will hand you the tracked files at a given commit with no repository attached.

git archive --format=tar HEAD | tar -x -C /path/to/release

What lands is the files and nothing else. No .git, and nothing that is untracked or ignored, which also catches the local scratch files that were never meant to go anywhere.

If you deploy with rsync from a working copy instead, exclude the folder explicitly and delete what is no longer wanted:

rsync -az --delete --exclude='.git/' --exclude='.env' ./ user@server:/var/www/site/

The trailing slashes matter and the exclusions are not optional. An rsync without --exclude is exactly how the folder gets there.

2. Build somewhere else and ship the output

The repository lives with your hosting provider or your continuous integration service. It checks the code out, runs whatever build the site needs, and copies only the result to the server. The web server never has a repository and never has a credential that could reach one.

This is what every managed platform does by default, and it is why a site deployed through one of them does not have this problem. If your host offers deployment from a repository, using it is a straight improvement over pulling on the server yourself.

The other benefit is that the deploy stops depending on the state of one machine. What is on the server is a known output of a known commit rather than whatever the last person to log in happened to leave behind.

For anything with a build step or a database migration, deploy into a dated directory and switch a symlink when it is ready.

/var/www/site/releases/2026-09-09-1420/
/var/www/site/shared/          # uploads, logs, the environment file
/var/www/site/current -> releases/2026-09-09-1420

The virtual host points at current, or at current/public for a framework that has one. The switch is atomic, so nobody sees a half copied site, and rolling back is repointing the symlink at the previous release.

Each release is an export rather than a clone, so no release contains a repository. Anything that must survive a deploy lives in shared and is linked into each release.

The other half of the problem

Keep secrets out of the repository entirely.

Everything on this website is about a repository being readable. The reason that is an emergency rather than an embarrassment is nearly always that the repository has credentials in it. Take those out and an exposed repository is a bad day instead of an incident.

  • Configuration comes from the environment, not from a committed file. The environment file lives on the server, outside the web root where possible, and is never committed.
  • Commit an example instead. A file listing the names of every setting with the values left blank documents what is needed without carrying anything sensitive.
  • Ignore the real one on day one. Adding it to the ignore file after it has been committed once does nothing at all, because the commit is already in the history.
  • Have something check. A pre commit hook or a scan in your pipeline that refuses a commit containing something shaped like a key catches this at the only moment it is cheap to fix.

If a secret is already in the history, treat it as published and change it. Rewriting history to remove it is possible, it is disruptive for everyone else working on the project, and it does not help with any copy that has already been taken. Rotating the credential is the step that actually makes the leak harmless.

Moving over without an outage

How to change this on a live site

  1. Block the folder now

    The rules on the fix page take a minute and stop the leak while you sort out the rest. Do not wait for the deploy change to be ready.

  2. Find out what is actually on the server

    Untracked files, uploads and local edits accumulate on a machine that has been pulled to for years. Compare it against the repository before you replace it, or the first clean deploy will remove something nobody knew was load bearing.

  3. Deploy alongside, not on top

    Put the first export in a new directory and leave the existing site running. Point a staging name at the new one and check it properly.

  4. Switch, and keep the old copy

    Change the document root or the symlink. Keep the previous directory for a week, with the folder still blocked, so a rollback is a repoint rather than a rebuild.

  5. Remove the credential from the server

    The server no longer needs to reach your repository, so take away the deploy key or token it was using, and revoke it at the other end rather than only deleting the local copy.

If the deploy is the finding

A repository in a web root is usually a symptom. The cause is that the way the site reaches the server grew rather than being designed, and the same root tends to produce the rest of it: no staging, no rollback, backups nobody has restored from, and one machine whose exact state is not written down anywhere.

That is infrastructure work rather than a security report. Dalaric does it: AWS architecture and multi server estates, Cloudflare, Linux, database performance, backup and disaster recovery, and security hardening. Planned work, scoped before it starts.

Short answers

Questions people actually ask

What is actually wrong with deploying by git pull?

Two things, and the leak is only the first. It puts the whole history inside the directory your web server publishes. It also means the web server holds a credential that can reach your repository, which is a second thing worth not having on a machine that faces the internet.

Can I just delete the .git folder on the server after pulling?

It works until somebody forgets, and somebody forgets. It also breaks the reason people deploy by pulling, which is that the next deploy is one command. Exporting the files instead takes the same one command and cannot leave anything behind.

I have no build step. Is this still worth changing?

Yes, and it is a one line change. git archive hands you the tracked files at a given commit with no repository attached, so nothing else about your workflow has to move.

Do I need continuous integration to do this properly?

No. Exporting from your own machine and copying the result up is a complete answer for a small site. The reason to build elsewhere is that it stops the deploy depending on the state of one laptop, which matters more as soon as two people are involved.

What about WordPress?

WordPress has no public directory, so the whole installation is the web root and a repository at the top of it is inside the served area by construction. Use the server rules, and treat changing the deploy as the real answer.

How do I move a live site over without an outage?

Block the folder first, so the leak stops while you work. Then deploy alongside the running site rather than on top of it, check it on a staging name, and switch the document root or the symlink when you are satisfied. Keep the old directory for a week so a rollback is a repoint rather than a rebuild.

Every question on this site, in one list

A password is in your history

Why changing it is the fix and rewriting history is not.

Undoing almost anything

One table, from unstaging a file to recovering a commit you deleted.

Why your ignore file is not working

Because it does nothing about a file git has already been told to track.

All nine guides