Git Pull vs Git Push Deployments: When to Use Each

Devops & Infrastructure, Tips & Tricks, and Tutorials

Git Pull vs Git Push Deployments: When to Use Each

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 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. 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:

  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 pullgit 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

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

  1. Go to Servers & Groups → New Server
  2. Choose Shell 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 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 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, 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:

  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 problemgit 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 prodcomposer 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 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.