How to Deploy with Git: Web UI, API, and GitHub Actions

Devops & Infrastructure, Git, and Tutorials

How to Deploy with Git: Web UI, API, and GitHub Actions

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 22 for SSH
  • Username: The SSH user (e.g., deploy or www-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., main for production, develop for 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:

  1. Preparing — checks server connectivity, checks out the target revision
  2. Building — runs your build pipeline (if configured)
  3. Transferring — connects to your server, runs pre-deployment commands, uploads changed files, runs post-deployment commands
  4. 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_keys on your server
  • If your server restricts SSH key types, you may need to allow ssh-rsa in your sshd_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.json or composer.json is valid
  • If you see JavaScript heap out of memory, your build exceeds the default Node.js memory limit — add export NODE_OPTIONS=--max-old-space-size=4096 before your build command
  • For version conflicts, specify exact versions in .deploybuild.yaml rather 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 current symlink 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.