Git pull deployment is the oldest trick in the deploy-from-Git playbook: SSH into the server, run `git pull`, restart the service. It's simple, it's free, and it works — until it doesn't. This guide shows you exactly how to set up a Git pull deployment (with and without an [automated deployment tool](https://www.deployhq.com/) managing the SSH side), where the pattern breaks, and when you should graduate to a push-based deployment instead.

## Pull deployments vs push deployments: a 30-second primer

There are two ways code gets from your Git host (GitHub, GitLab, Bitbucket) onto your production server:

| | **Pull** (server fetches) | **Push** (CI fetches and ships) |
| --- | --- | --- |
| Who initiates | Your server runs `git pull` (manually, on cron, or via webhook listener) | A CI/CD service like [DeployHQ](https://www.deployhq.com/deploy-from-github) clones, builds, and ships the result. See [what continuous deployment actually means](https://www.deployhq.com/blog/what-is-continuous-deployment) for the broader pattern |
| Build step | Has to run on the production server | Runs in CI, only the artifact lands on the server |
| Server needs | Git installed, deploy key, network access to Git host | Just an SSH endpoint |
| Failure mode | Merge conflicts, partial pulls, secrets on every server | Network blips during transfer |
| Atomicity | Hard (you're mutating the live tree) | Easy (deploy to a new release dir, swap a symlink) |
| Rollback | `git reset --hard <prev-sha>` and pray | [One-click rollback](https://www.deployhq.com/features/one-click-rollback) to the last good release |
| Transfer method | Whatever your Git host's SSH does | SSH/SFTP — see [SFTP vs SCP vs rsync](https://www.deployhq.com/blog/sftp-vs-scp-vs-rsync-choosing-the-right-file-transfer-method) for the tradeoffs |
| Multi-server | Fans out N pulls from your Git host | One build, parallel push to N servers |

If your stack is a single PHP/Python/Ruby box with no build step and one server, pull works fine. If you have a build step (Webpack, Vite, Composer, npm), multiple servers, or compliance requirements that keep deploy keys off prod, you want push. We'll come back to this.

## When Git pull deployments make sense

Be honest about which side you're on before you spend an afternoon configuring this:

- **Good fit** : WordPress, Laravel without a heavy front-end build, static PHP sites, internal tools, a personal VPS, prototypes
- **Bad fit** : Anything with a `dist/` directory, anything where you want zero-downtime, anything with more than two app servers, anything regulated, anything where I'll just SSH in and fix it is a thing that happens

If you're in the bad fit column, skip ahead to the [push-based alternative](#the-push-based-alternative). Otherwise, let's set up a pull deployment properly.

## Method 1: Plain Git pull (no platform)

This is the baseline. You're going to clone the repo on the server once, then run `git pull` whenever you want to deploy.

### Step 1: Generate a deploy key

On the server, generate an SSH key that has read-only access to the repo:

```
ssh-keygen -t ed25519 -C "deploy@$(hostname)" -f ~/.ssh/deploy_key -N ""
cat ~/.ssh/deploy_key.pub
```

Paste the public key into your repo's deploy keys settings (GitHub: Settings → [Deploy](https://www.deployhq.com) keys; GitLab: Settings → Repository → [Deploy](https://www.deployhq.com) keys; Bitbucket: Repository settings → Access keys). Leave Allow write access **off** — read-only is the whole point of a deploy key.

Add an SSH config entry so Git uses this key for this repo only:

```
cat >> ~/.ssh/config <<'EOF'
Host github-deploy
  HostName github.com
  User git
  IdentityFile ~/.ssh/deploy_key
  IdentitiesOnly yes
EOF
chmod 600 ~/.ssh/config
```

### Step 2: Clone the repo

```
cd /var/www
git clone git@github-deploy:your-org/your-repo.git app
cd app
```

### Step 3: Write the deploy script

```
cat > /var/www/app/deploy.sh <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

cd /var/www/app
git fetch --prune origin
git reset --hard origin/main
git clean -fd

# Install deps, run migrations, restart services
# Adjust to your stack:
# composer install --no-dev --optimize-autoloader
# php artisan migrate --force
# sudo systemctl reload php8.3-fpm
EOF
chmod +x /var/www/app/deploy.sh
```

Three things to notice:

1. **`set -euo pipefail`** — fail fast on any error, undefined variable, or pipeline failure. Without this, a half-broken deploy will exit 0 and you'll never know
2. **`git reset --hard origin/main`** instead of `git pull` — `git pull` does a merge, which can fail if someone hot-fixed a file on the server. `reset --hard` makes the working tree match the remote, period. Local changes on the server are obliterated by design (this is a feature, not a bug — production servers shouldn't have local Git state)
3. **`git clean -fd`** — removes untracked files. If you ever delete a file in the repo, `git pull` alone won't remove it from the server; `clean -fd` will

### Step 4: Trigger the deploy

Three common patterns:

- **Manual** : SSH in, run `./deploy.sh`. Honest, controllable, doesn't scale past two engineers
- **Cron** : `*/5 * * * * /var/www/app/deploy.sh >> /var/log/deploy.log 2>&1`. The classic poor man's CD. Wastes Git host API quota, introduces 5-minute drift between merge and deploy, and silently fails if cron sends mail nobody reads
- **Webhook listener** : A small HTTP service on the server that runs `deploy.sh` on a GitHub `push` event. Better, but now you maintain a webhook receiver and its TLS cert — and you've essentially built a worse version of [DeployHQ's webhook-triggered deployments](https://www.deployhq.com/features/automatic-deployments)

## Method 2: Git pull orchestrated through DeployHQ's Shell target

If you want the pull pattern but also want logs, deployment history, Slack notifications, manual deploy approvals, and the option to fan out to multiple servers without rewriting your script, you can have [DeployHQ](https://www.deployhq.com) run the pull for you. It's still a pull deployment — [DeployHQ](https://www.deployhq.com) just becomes the thing that SSHes in and triggers it.

### Step 1: Create a project

In your [DeployHQ](https://www.deployhq.com) dashboard, click **New Project** , connect your Git host, and pick your repo. [DeployHQ](https://www.deployhq.com) supports GitHub, [GitLab deployments](https://www.deployhq.com/deploy-from-gitlab), Bitbucket, [Codebase](https://www.codebasehq.com), and self-hosted Git. The [continuous delivery vs continuous deployment](https://www.deployhq.com/blog/continuous-delivery-vs-continuous-deployment) breakdown is worth a read here — it determines whether you let pushes auto-deploy or gate them behind a manual approval.

### Step 2: Add a Shell server

1. Go to **Servers & Groups → New Server**
2. Choose [Shell](https://www.deployhq.com/support/servers/adding-a-server/shell-server) as the deployment target
3. Fill in:
  - **Hostname** : server IP or DNS name
  - **Username** : the deploy user (not root)
  - **Authentication** : SSH key (DeployHQ generates a public key for you — add it to `~/.ssh/authorized_keys` for the deploy user on the server)
  - **Remote path** : where the repo lives, e.g. `/var/www/app`

The Shell target tells [DeployHQ](https://www.deployhq.com) I don't want you to upload built files — just SSH in and run these commands.

### Step 3: Configure deployment commands

Under your project's **Configuration → SSH Commands** , add a _Before deployment_ command (or use _After upload_ if you have a no-op upload):

```
cd /var/www/app
git fetch --prune origin
git reset --hard %revision%
git clean -fd
composer install --no-dev --optimize-autoloader 2>&1 || exit 1
php artisan migrate --force 2>&1 || exit 1
sudo systemctl reload php8.3-fpm
```

Note the `%revision%` placeholder — [DeployHQ](https://www.deployhq.com) substitutes the exact commit SHA being deployed. That's better than `origin/main` because it lets you redeploy an older commit from the [DeployHQ](https://www.deployhq.com) UI without ambiguity.

### Step 4: Enable automatic deployments (optional)

Under **Deployment Settings** , toggle **Automatic Deployments**. [DeployHQ](https://www.deployhq.com) wires up a webhook on your Git host so a push to your deploy branch triggers a deployment automatically. Same effect as a self-hosted webhook listener, minus the listener.

### What you get vs the plain pull

| | Plain pull | Pull via DeployHQ Shell target |
| --- | --- | --- |
| Deployment history | grep your log file | Full audit log with diffs |
| Notifications | None | Slack, email, webhook |
| Rollback | Manual `git reset` | One-click rollback to a previous release |
| Multi-server fan-out | Custom script | Built-in server groups |
| Build step | On prod (slow, risky) | Still on prod (this is pull's ceiling) |
| Zero downtime | No | No (you'd need a release-directory pattern, which requires push) |

If you start needing the bottom two rows, you've outgrown pull deployments.

## The push-based alternative

The moment you have a real build step or more than one app server, pull deployments start costing you more than they save. Here's why teams switch:

- **Build artifacts** : `npm run build` produces `dist/` files that aren't in Git. Pull deployments either commit `dist/` (a sin) or build on the production server (slow, requires Node/Composer/Python on prod, blocks the request pipeline during the build)
- **Atomic releases** : with pull, you're mutating files in place. Users hit the site mid-`git pull` and get a broken state. The industry-standard fix is the **Capistrano-style release directory pattern** — deploy into `releases/<timestamp>/`, then swap a `current` symlink. This is hard to do from a `git pull` and easy to do from a build pipeline that copies a fresh artifact
- **Multi-server consistency** : pull from 8 servers, you get 8 simultaneous reads against your Git host, and one might fail mid-pull. Push from one CI run, you ship the _exact same bytes_ to 8 servers
- **Secrets hygiene** : pull requires a deploy key on every production server. Push keeps Git credentials in CI and only needs SSH access to the prod boxes
- **Recovery time objective** : `git reset --hard <sha>` works if the previous SHA's runtime dependencies are still installed. A push-based platform keeps actual previous release artifacts ready, which is why rollback is measured in seconds rather than however long `composer install` takes

If any of those bullets describe a thing that's bitten you in the last quarter, you want push. DeployHQ's default mode is push-based — it clones your repo on its build servers, runs your [build pipeline](https://www.deployhq.com/features/build-pipelines), then SSHes only the final artifact to your servers. For a deeper comparison of every Git-based deploy method [DeployHQ](https://www.deployhq.com) supports, see [how to deploy with Git via web UI, API, and GitHub Actions](https://www.deployhq.com/blog/mastering-git-deployments-with-deployhq-a-comprehensive-guide).

## Git pull deployment gotchas (the things nobody tells you)

After years of supporting customers running pull deployments, here's the failure list:

1. **Divergent local commits on the server** — someone SSHes in, edits a config file, commits it locally. Next `git pull` fails with a merge conflict, the deploy hangs, and nothing tells you. Fix: always use `git reset --hard` instead of `git pull`, and treat the prod working tree as read-only
2. **The deleted-file problem** — `git pull` doesn't remove files you've deleted from the repo. A stale `routes/old-thing.php` keeps responding to requests. Fix: `git clean -fd` after every reset
3. **The `.env` problem** — your `.env` is gitignored, so pull never touches it. But when you `git clean -fd`, you'll wipe untracked files. Either keep `.env` outside the repo root or use `git clean -fdx` carefully with explicit exclude patterns
4. **The half-pulled state** — if the connection drops mid-pull, the working tree is a Frankenstein. Without atomic releases, your users are now hitting a broken commit. There's no good fix at the pull layer; this is why atomic deployments require push
5. **The 3 AM cron failure** — cron-driven pulls don't surface errors. The deploy fails, the site stays on the old commit, and you find out from a customer. Fix: pipe to a real logging service, or move to a deploy platform with notifications
6. **Build step on prod** — `composer install` or `npm ci` on a live server competes with PHP-FPM / Node workers for CPU and memory. Big enough deploys cause measurable response-time spikes. Fix: build elsewhere (push deployments) or schedule deploys for low-traffic windows
7. **The forgotten deploy key** — server gets rebuilt, deploy key isn't reinstalled, next pull fails silently. Fix: bake key provisioning into your server image (Ansible, Packer) so this can't drift

## When to graduate to push deployments

You've outgrown pull deployments if any of these are true:

- You have a build step that takes \>30 seconds
- You run more than 2 app servers
- You can't afford visible downtime during deploys
- You need an audit trail of who deployed what and when
- You've ever rolled back by SSHing to multiple servers in parallel
- Compliance requires that production servers not have outbound access to Git hosts

When you're there, the simplest migration path is to set up a [DeployHQ](https://www.deployhq.com) project that **builds in CI** (your existing `composer install` / `npm run build` runs on DeployHQ's build servers, not on prod) and **pushes the built artifact** to your existing Shell target. You keep the same servers and the same SSH user; you just stop pulling from Git on the prod box and start receiving pre-built files instead. For the why-and-when of automating that switch, see our breakdown of [Git auto deployment: when it's a game changer and when it's a gamble](https://www.deployhq.com/blog/git-auto-deployment-when-it-s-a-game-changer-and-when-it-s-a-gamble).

## Where DeployHQ fits

Whichever pattern you pick, [DeployHQ](https://www.deployhq.com) can run it:

- **Pull pattern** : configure a Shell target with the commands above. [DeployHQ](https://www.deployhq.com) becomes the thing that triggers, logs, and audits your `git pull`
- **Push pattern (recommended for most teams)**: let [DeployHQ](https://www.deployhq.com) build and ship the artifact. You get [zero-downtime deployments](https://www.deployhq.com/features/zero-downtime-deployments), instant rollback, parallel multi-server fan-out, and build caching that turns a 5-minute build into a 30-second one
- **From your terminal** : if you're a CLI-first team, [DeployHQ Agents](https://www.deployhq.com/agents) lets you trigger and inspect deployments from a local command-line tool — same authentication, same audit log

[Start a free](https://www.deployhq.com/signup)[DeployHQ](https://www.deployhq.com) trial and you can have either pattern running in under 30 minutes — and switch between them whenever your stack outgrows the simpler one. Pricing tiers are documented on the [DeployHQ pricing page](https://www.deployhq.com/pricing), and agency teams managing many client servers should look at [DeployHQ for agencies](https://www.deployhq.com/for-agencies).

* * *

Need help picking between pull and push, or migrating from one to the other? Email our team at [support@deployhq.com](mailto:support@deployhq.com) or follow us at [@deployhq](https://x.com/deployhq) for deployment tips.

