Every time you deploy code by dragging files into an FTP client, you're taking a risk. Missed files, overwritten configs, minutes of downtime while half your changes are live and the other half aren't — it adds up. Git-based deployment eliminates these problems by treating your repository as the single source of truth for what's on your server.
This guide walks you through three ways to deploy from Git using DeployHQ: the web interface for quick manual deploys, the API for scripted workflows, and GitHub Actions for fully automated CI/CD. We'll also cover zero-downtime (atomic) deployments, build pipelines, and how to troubleshoot the most common failures.
Prerequisites
Before you start, you'll need:
- A DeployHQ account (free tier works for testing)
- A Git repository hosted on GitHub, GitLab, Bitbucket, or any Git host accessible via SSH
- An SSH/SFTP server to deploy to (a VPS, shared hosting, or cloud instance)
- Basic familiarity with Git commands (
push,pull,commit)
Method 1: Deploy from the Web Interface
The fastest way to get started. You connect your repository, add a server, and deploy — all from the browser.
Step 1: Create a Project
In your DeployHQ dashboard, click New Project. Select your repository host (GitHub, GitLab, Bitbucket, or manual). If you're using GitHub or GitLab, DeployHQ connects via OAuth — it automatically adds a deploy key and webhook to your repository.
For manual repositories, you'll paste your SSH clone URL (e.g., git@github.com:yourorg/yourapp.git) and install the provided public key as a deploy key on your host.
Step 2: Add a Server
Navigate to Servers in your project sidebar and click New Server. You'll configure:
- Protocol: SSH/SFTP (recommended), FTP, or FTPS
- Hostname: Your server's IP or domain (e.g.,
203.0.113.10) - Port: Usually
22for SSH - Username: The SSH user (e.g.,
deployorwww-data) - Authentication: Public key (most secure), auto-install script, or password
- Deployment path: The absolute path on your server (e.g.,
/var/www/myapp) - Branch to deploy from: The Git branch this server tracks (e.g.,
mainfor production,developfor staging)
If you're deploying to multiple environments, create separate servers for each — one tracking main, another tracking staging. DeployHQ only deploys when a push matches the server's configured branch.
Step 3: Run Your First Deployment
Go to Deployments > New Deployment. DeployHQ shows you the list of commits that will be deployed. Click Deploy to start.
The deployment runs through four stages:
- Preparing — checks server connectivity, checks out the target revision
- Building — runs your build pipeline (if configured)
- Transferring — connects to your server, runs pre-deployment commands, uploads changed files, runs post-deployment commands
- Finishing — sends notifications, logs the result
DeployHQ only transfers files that changed between the last deployed commit and the new one — not the entire repository. A typical deployment with a handful of changed files completes in under 10 seconds.
Step 4: Enable Automatic Deployments
Under Automatic Deployments in your project sidebar, you'll find a unique webhook URL. When you connected your repository via OAuth, DeployHQ already installed this webhook — so every git push to a matching branch triggers a deployment automatically.
You can toggle automatic deployments per server. This is useful when you want staging to auto-deploy on push but production to require a manual trigger.
Method 2: Deploy via the API
For scripted workflows, CI pipelines, or custom tooling, DeployHQ's REST API gives you full control over deployments programmatically.
Authentication
The API uses HTTP Basic Auth. Your username is your DeployHQ email, and your password is your API key (found under Settings > Security).
# Test your credentials
curl -s -u "you@example.com:your-api-key" \
-H "Accept: application/json" \
https://yourteam.deployhq.com/projects \
| python3 -m json.tool
Trigger a Deployment
curl -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-u "you@example.com:your-api-key" \
-d '{
"deployment": {
"parent_identifier": "YOUR-SERVER-UUID",
"start_revision": "",
"end_revision": "latest",
"branch": "main",
"mode": "queue",
"copy_config_files": true,
"run_build_commands": true
}
}' \
https://yourteam.deployhq.com/projects/my-project/deployments
Key parameters:
| Parameter | Description |
|---|---|
parent_identifier |
UUID of the server or server group (find it via GET /projects/<project>/servers) |
start_revision |
The commit to diff from. Leave blank to deploy the entire branch from scratch |
end_revision |
The target commit, or "latest" for the newest commit on the branch |
mode |
"queue" to deploy immediately, "preview" to see what would change |
copy_config_files |
Include config files stored in DeployHQ (database credentials, .env files) |
run_build_commands |
Execute your build pipeline before deploying |
Schedule a Deployment
You can schedule deployments for off-peak hours:
curl -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-u "you@example.com:your-api-key" \
-d '{
"deployment": {
"parent_identifier": "YOUR-SERVER-UUID",
"start_revision": "",
"end_revision": "latest",
"branch": "main",
"mode": "queue"
},
"schedule": {
"frequency": "weekly",
"weekly": {
"weekday": "Sunday",
"hour": "3",
"minute": "0"
}
}
}' \
https://yourteam.deployhq.com/projects/my-project/deployments
Supported frequencies: future (one-time), daily, weekly, and monthly.
Rollback via API
If something goes wrong, roll back to the previous deployment:
curl -X POST \
-H "Content-Type: application/json" \
-H "Accept: application/json" \
-u "you@example.com:your-api-key" \
-d '{
"mode": "queue",
"copy_config_files": true,
"run_build_commands": true
}' \
https://yourteam.deployhq.com/projects/my-project/deployments/DEPLOYMENT-UUID/rollback
The rollback creates a new deployment that reverses the changes — added files are removed, deleted files are restored, and modified files revert to their previous state.
Full API reference with interactive examples: api.deployhq.com/docs
Method 3: Automated CI/CD with GitHub Actions
For teams that want deployments triggered only after tests pass, DeployHQ integrates directly with GitHub Actions via the official deployhq/deployhq-action.
Basic Workflow: Deploy on Push
Create .github/workflows/deploy.yml in your repository:
name: Deploy to production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Trigger DeployHQ deployment
uses: deployhq/deployhq-action@main
env:
DEPLOYHQ_WEBHOOK_URL: ${{ secrets.DEPLOYHQ_WEBHOOK_URL }}
DEPLOYHQ_EMAIL: ${{ secrets.DEPLOYHQ_EMAIL }}
REPO_CLONE_URL: ${{ secrets.REPO_CLONE_URL }}
Add three secrets to your repository (Settings > Secrets and variables > Actions):
| Secret | Where to find it |
|---|---|
DEPLOYHQ_WEBHOOK_URL |
DeployHQ project > Automatic Deployments page |
DEPLOYHQ_EMAIL |
Your DeployHQ account email |
REPO_CLONE_URL |
The repository path exactly as shown in DeployHQ (e.g., git@github.com:yourorg/yourapp.git) |
Important: After setting up the GitHub Action, delete the auto-added DeployHQ webhook from your GitHub repository (Settings > Webhooks). Otherwise, every push triggers two deployments — one from the webhook and one from the Action.
Advanced Workflow: Test, Then Deploy
The real power of GitHub Actions is gating deployments behind your test suite:
name: Test and deploy
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm test
- run: npm run build
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- name: Trigger DeployHQ deployment
uses: deployhq/deployhq-action@main
env:
DEPLOYHQ_WEBHOOK_URL: ${{ secrets.DEPLOYHQ_WEBHOOK_URL }}
DEPLOYHQ_EMAIL: ${{ secrets.DEPLOYHQ_EMAIL }}
REPO_CLONE_URL: ${{ secrets.REPO_CLONE_URL }}
The needs: test dependency ensures the deploy job only runs if all tests pass. If your test suite fails, the deployment never fires.
Staging + Production with Manual Approval
For teams that deploy to staging automatically but require approval for production:
name: Deploy pipeline
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm ci && npm test
deploy-staging:
needs: test
runs-on: ubuntu-latest
steps:
- name: Deploy to staging
uses: deployhq/deployhq-action@main
env:
DEPLOYHQ_WEBHOOK_URL: ${{ secrets.DEPLOYHQ_STAGING_WEBHOOK }}
DEPLOYHQ_EMAIL: ${{ secrets.DEPLOYHQ_EMAIL }}
REPO_CLONE_URL: ${{ secrets.REPO_CLONE_URL }}
deploy-production:
needs: deploy-staging
runs-on: ubuntu-latest
environment: production
steps:
- name: Deploy to production
uses: deployhq/deployhq-action@main
env:
DEPLOYHQ_WEBHOOK_URL: ${{ secrets.DEPLOYHQ_PRODUCTION_WEBHOOK }}
DEPLOYHQ_EMAIL: ${{ secrets.DEPLOYHQ_EMAIL }}
REPO_CLONE_URL: ${{ secrets.REPO_CLONE_URL }}
The environment: production line connects to a GitHub Environment with required reviewers. After staging deploys, a team member must approve the production deployment in the GitHub Actions UI.
Each DeployHQ server has its own webhook URL, so you use separate secrets (DEPLOYHQ_STAGING_WEBHOOK vs DEPLOYHQ_PRODUCTION_WEBHOOK) to target different environments.
Zero-Downtime (Atomic) Deployments
Standard deployments upload files directly into your web root. During the transfer, your site runs a mix of old and new files — which can cause errors if a new template references a new CSS file that hasn't been uploaded yet.
Atomic deployments solve this by uploading to a separate directory and switching a symlink only after the transfer completes.
How It Works
When you enable zero-downtime deployments on a server, DeployHQ creates this structure:
/var/www/myapp/
├── releases/
│ ├── 20260308120000/ # Previous release
│ └── 20260310143000/ # Current release
├── shared/ # Persistent files (uploads, logs)
├── cache/ # Clean repo copy (optional)
└── current -> releases/20260310143000 # Symlink
Each deployment creates a new timestamped directory under releases/. Files are uploaded there. Only after the transfer completes does the current symlink switch to the new release. Your web server's document root points to /var/www/myapp/current.
If the deployment fails, the symlink stays on the previous release — your users never see a broken state.
Two Atomic Strategies
DeployHQ offers two strategies when creating a new release:
| Strategy | How it works | Best for |
|---|---|---|
| Copy previous release | Copies the last release directory, then uploads only changed files | Apps where files might be modified on the server (user uploads in the release dir, runtime caches) |
| Copy from cache | Maintains a clean cache of the repo, copies it fresh each time | Apps where the release directory should always match the repo exactly |
Configuring Shared Files
Files that must persist across releases — like user uploads, .env files, or log directories — go in the shared/ directory and are symlinked into each release. Configure these under Server settings > Shared files.
SSH Commands with Atomic Deployments
Your deployment commands have access to special variables:
# Post-deployment: run migrations in the new release
cd %release_path% && php artisan migrate --force
# Clear cache using the shared path
cd %release_path% && php artisan config:cache
# Reference the previous release for comparison
diff %previous_release_path%/.env %release_path%/.env
| Variable | Points to |
|---|---|
%release_path% |
The new release being deployed |
%shared_path% |
The persistent shared directory |
%current_path% |
The active release (before the switch) |
%previous_release_path% |
The previous release |
Release Retention
DeployHQ keeps a configurable number of releases (default: 3). Older releases are automatically deleted. Rolling back is as simple as switching the symlink back to a previous release directory.
Build Pipelines
If your project needs a build step — compiling assets, installing dependencies, running a bundler — DeployHQ runs these commands on its own build servers before transferring files.
Quick Setup
Under Build Pipeline in your project, select your language and version (e.g., Node.js 20, PHP 8.3), then add your commands:
npm ci
npm run build
Built files (like dist/ or public/build/) are included in the deployment automatically. Dependencies that are only needed for building (like node_modules/) can be excluded via your project's excluded files list.
Build-as-Code with deploybuild.yaml
Instead of configuring builds in the UI, you can define your pipeline in a .deploybuild.yaml file at the root of your repository:
build_languages:
- name: "node"
version: "20"
- name: "php"
version: "8.3"
build_commands:
- description: "Install and build"
command: "composer install --no-dev --optimize-autoloader \n npm ci \n npm run build"
halt_on_error: true
build_cache_files:
- path: node_modules/**
- path: vendor/**
This approach has a key advantage: different branches can have different build configs. Your main branch might run a production build while develop runs a development build with source maps.
Build Caching
Enable caching for node_modules/, vendor/, or other dependency directories to avoid re-downloading them on every deployment. Under Build Configuration > Cached Build Files, add glob patterns like node_modules/**. A typical Node.js build drops from 45 seconds to under 10 seconds with caching enabled.
If you need a clean build (e.g., after updating Node.js version), check the Clean build option when creating a deployment.
Troubleshooting Common Failures
Connection refused
or Permission denied
- Verify your server's hostname and port are correct
- Check that DeployHQ's public key is installed in
~/.ssh/authorized_keyson your server - If your server restricts SSH key types, you may need to allow
ssh-rsain yoursshd_config— see PubkeyAcceptedAlgorithms troubleshooting - Ensure the deployment path exists and is writable by the SSH user
Build Pipeline Fails
- Check the build log for the exact error — it's usually a missing dependency or incompatible version
- Verify your
package.jsonorcomposer.jsonis valid - If you see
JavaScript heap out of memory
, your build exceeds the default Node.js memory limit — addexport NODE_OPTIONS=--max-old-space-size=4096before your build command - For version conflicts, specify exact versions in
.deploybuild.yamlrather than relying on UI defaults
No files to deploy
This happens when DeployHQ can't detect changes between the start and end revision. Common causes:
- The branch was force-pushed and the old commit SHA no longer exists — run a deploy from scratch
- The repository was re-cached — go to Repository > Recache in your project settings
- Build output isn't being included — check that your build pipeline runs before deployment and that output directories aren't in your excluded files list
Automatic Deployments Not Triggering
- Verify the webhook is installed: check your repository's webhook settings for the DeployHQ URL
- Confirm the push was to a branch that matches a server's Branch to deploy from setting
- If using GitHub Actions, make sure you deleted the auto-added webhook to avoid conflicts
- Check DeployHQ's Automatic Deployments page for any recent webhook payloads and their status
Zero-Downtime Deployment Stuck
- Ensure your server supports symlinks (
ln -s) — this won't work on basic shared hosting with FTP-only access - Check that the deployment path parent directory is writable
- If the
currentsymlink exists but points to a missing directory, delete it manually via SSH and run a fresh deployment
Frequently Asked Questions
How does DeployHQ know which files changed? DeployHQ diffs the previously deployed commit against the new one using Git. Only files that were added, modified, or deleted between those two commits are transferred. This makes deployments fast — typically under 10 seconds for a handful of changed files.
Can I deploy to multiple servers at once? Yes. Create a server group and add your servers to it. You can deploy in parallel (all servers at once) or sequentially (batches). Parallel mode is recommended when using zero-downtime deployments to keep all servers in sync.
What if I need to deploy different parts of a monorepo to different servers?
Use server groups with per-server excluded files. For example, exclude everything except frontend/dist/** on your CDN server, and exclude everything except api/ on your backend server. The build pipeline runs once, and DeployHQ filters the file manifest per server.
Can I use DeployHQ without giving it access to my repository? Yes. Instead of OAuth, you can add a server manually and install DeployHQ's public key as a read-only deploy key. DeployHQ never writes to your repository.
Is there a way to preview changes before deploying?
Set mode to "preview" when creating a deployment (via UI or API). DeployHQ generates a full list of files that would be added, modified, or deleted — without actually transferring anything.
Git-based deployment is the foundation of a reliable release process — no more FTP guesswork, no more which files did I change?
moments. Whether you start with the web interface and work your way up to automated CI/CD, DeployHQ handles the mechanics so you can focus on shipping code.
Ready to try it? Create a free DeployHQ account and deploy your first project in under five minutes.
Have questions? Reach out at support@deployhq.com or find us on Twitter/X.