# Deploying with a post-receive Git Hook

LLMS index: [llms.txt](/en/llms.txt)

---

Deploying through CI is great, but sometimes you just need to push code to a server with a single `git push`. The `post-receive` hook in a bare repository handles this without extra dependencies: push to the server, and the hook automatically checks out files into the working directory.

## Schema: bare repo as a deploy trigger

The logic is straightforward:

1. A bare repository is created on the server (e.g., `/srv/deploy/app.git`).
2. The developer adds it as a remote and runs `git push origin main`.
3. Git receives the data and triggers `hooks/post-receive`.
4. The hook script runs `git --work-tree=/var/www/app --git-dir=/srv/deploy/app.git checkout -f main`.

> [!NOTE]
> A bare repository has no working directory. That is why `post-receive` must explicitly pass `--work-tree` so `checkout` knows where to write files.

This is not CI — it is a direct trigger at the Git level. No pipelines, artifacts, or queues. It fits small services, VPS instances, and internal tools well.

## Creating a bare repository on the server

SSH into the server and create the repo:

```bash
mkdir -p /srv/deploy
cd /srv/deploy
git init --bare app.git
```

The resulting structure is standard: `hooks/`, `objects/`, `refs/`, `HEAD`, `config`. Default hooks live in `/srv/deploy/app.git/hooks/` with a `.sample` suffix — rename them or create your own.

> [!WARNING]
> Make sure `/srv/deploy` is owned by the user you are pushing as. Otherwise, write permissions on `objects/` will be denied.

## Writing the post-receive hook

Create `/srv/deploy/app.git/hooks/post-receive`:

```bash
#!/usr/bin/env bash
set -euo pipefail

REPO_DIR="/srv/deploy/app.git"
WORK_TREE="/var/www/app"
BRANCH="main"

while read oldrev newrev refname; do
  if [ "$refname" = "refs/heads/$BRANCH" ]; then
    echo "Deploying $BRANCH to $WORK_TREE..."
    git --work-tree="$WORK_TREE" --git-dir="$REPO_DIR" checkout -f "$BRANCH"
    echo "Deployment complete."
  fi
done
```

Key elements:

| Element | Purpose |
|---|---|
| `set -euo pipefail` | Script exits on any error; does not continue with bad data |
| `while read oldrev newrev refname` | The hook passes three arguments per line for each updated ref |
| `refs/heads/$BRANCH` | Checks that the pushed ref is the target branch |
| `checkout -f` | Force-synchronizes the working tree with the reference |

> [!TIP]
> If you need to restart a service after deployment, append `systemctl restart app` or `supervisorctl reload app` to the end of the script. The hook runs in the context of the Git user, so verify it has permission to restart.

## Setting up permissions and access

The most common issue is permissions. The Git user you push as must be able to write to `WORK_TREE` and execute commands from the hook.

Setup options:

- **Single user**: Git user and web user are the same identity. Simply `chown -R deploy:deploy /srv/deploy /var/www/app`.
- **Group**: Add the Git user to the web server group. `usermod -aG www-data deploy`, then `chmod -R g+w /var/www/app`.
- **SSH key**: Push over SSH; the key authenticates as the correct user.

```bash
# Example: deploy user, web root /var/www/app
sudo useradd -m deploy
sudo mkdir -p /var/www/app
sudo chown -R deploy:deploy /var/www/app
sudo chmod -R 755 /var/www/app
```

Do not forget to make the hook executable:

```bash
chmod +x /srv/deploy/app.git/hooks/post-receive
```

## Pushing from the client and verifying

On the developer machine, add the remote and push:

```bash
git remote add production deploy@server:/srv/deploy/app.git
git push production main
```

On the server, the hook log will show:

```
Deploying main to /var/www/app...
Deployment complete.
```

Verify files on the server:

```bash
ls -la /var/www/app
git --git-dir=/srv/deploy/app.git --work-tree=/var/www/app status
```

> [!WARNING]
> If the push fails with `Permission denied`, check the owner of `hooks/post-receive` and the target directory. If `checkout` does not write files, confirm that `WORK_TREE` points to an existing directory and the user has write access to it.

That is it. No additional tools — just Git and shell. For high-load production, this is obviously not a replacement for a proper CI pipeline, but for quick deploys to one or two servers it works reliably without unnecessary complexity.
