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 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 clones, builds, and ships the result. See what continuous deployment actually means 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 to the last good release |
| Transfer method | Whatever your Git host's SSH does | SSH/SFTP — see SFTP vs SCP vs rsync 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 whereI'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. 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 keys; GitLab: Settings → Repository → Deploy 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:
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 knowgit reset --hard origin/maininstead ofgit pull—git pulldoes a merge, which can fail if someone hot-fixed a file on the server.reset --hardmakes 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)git clean -fd— removes untracked files. If you ever delete a file in the repo,git pullalone won't remove it from the server;clean -fdwill
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 classicpoor 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.shon a GitHubpushevent. 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
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 run the pull for you. It's still a pull deployment — DeployHQ just becomes the thing that SSHes in and triggers it.
Step 1: Create a project
In your DeployHQ dashboard, click New Project, connect your Git host, and pick your repo. DeployHQ supports GitHub, GitLab deployments, Bitbucket, Codebase, and self-hosted Git. The 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
- Go to Servers & Groups → New Server
- Choose Shell as the deployment target
- 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_keysfor the deploy user on the server) - Remote path: where the repo lives, e.g.
/var/www/app
The Shell
target tells DeployHQ 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 substitutes the exact commit SHA being deployed. That's better than origin/main because it lets you redeploy an older commit from the DeployHQ UI without ambiguity.
Step 4: Enable automatic deployments (optional)
Under Deployment Settings, toggle Automatic Deployments. DeployHQ 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 buildproducesdist/files that aren't in Git. Pull deployments either commitdist/(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 pulland get a broken state. The industry-standard fix is the Capistrano-style release directory pattern — deploy intoreleases/<timestamp>/, then swap acurrentsymlink. This is hard to do from agit pulland 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 thanhowever 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, then SSHes only the final artifact to your servers. For a deeper comparison of every Git-based deploy method DeployHQ supports, see how to deploy with Git via web UI, API, and GitHub Actions.
Git pull deployment gotchas (the things nobody tells you)
After years of supporting customers running pull deployments, here's the failure list:
- Divergent local commits on the server — someone SSHes in, edits a config file, commits it locally. Next
git pullfails with a merge conflict, the deploy hangs, and nothing tells you. Fix: always usegit reset --hardinstead ofgit pull, and treat the prod working tree as read-only - The deleted-file problem —
git pulldoesn't remove files you've deleted from the repo. A staleroutes/old-thing.phpkeeps responding to requests. Fix:git clean -fdafter every reset - The
.envproblem — your.envis gitignored, so pull never touches it. But when yougit clean -fd, you'll wipe untracked files. Either keep.envoutside the repo root or usegit clean -fdxcarefully with explicit exclude patterns - 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
- 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
- Build step on prod —
composer installornpm cion 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 - 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 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.
Where DeployHQ fits
Whichever pattern you pick, DeployHQ can run it:
- Pull pattern: configure a Shell target with the commands above. DeployHQ becomes the thing that triggers, logs, and audits your
git pull - Push pattern (recommended for most teams): let DeployHQ build and ship the artifact. You get 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 lets you trigger and inspect deployments from a local command-line tool — same authentication, same audit log
Start a free DeployHQ 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, and agency teams managing many client servers should look at DeployHQ for agencies.
Need help picking between pull and push, or migrating from one to the other? Email our team at support@deployhq.com or follow us at @deployhq for deployment tips.