Web Solutions

Push-to-Deploy with GitHub Actions: Automatic Deployment Guide

Set up push-to-deploy with GitHub Actions for static sites and apps: workflow basics, secrets, FTP, SSH and rsync deploys, caching, environments and rollback.

GPTLabAI team 7 min read

Push-to-deploy with GitHub Actions means every push to your main branch builds the project and ships it to the server automatically, with no manual FTP uploads and no “it worked on my machine”. You write one workflow file, store your server credentials as encrypted secrets, and GitHub runs the build and deploy on its own runners. This guide covers the workflow basics, three deploy methods (FTP, SSH and rsync), caching, environments and how to roll back when something breaks.

Why push-to-deploy with GitHub Actions

Manual deploys fail in predictable ways: someone forgets to run the build, uploads the wrong folder, or overwrites a config file on the server. Automating the process gives you:

  • Repeatability: the same steps run in the same order every time.
  • A record: every deploy is tied to a commit and a log you can read later.
  • Safety checks: tests and linting run before anything reaches production.
  • Speed: a small change goes live in a minute or two, without anyone opening an FTP client.

GitHub Actions is built into GitHub, so there is nothing extra to host. For most small and mid-sized projects it is the simplest way to get there.

Workflow basics

A workflow is a YAML file in .github/workflows/. It has triggers (on), one or more jobs, and steps inside each job. Here is a minimal build for a static site made with a Node-based tool such as Astro or Vite:

name: Deploy

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: deploy-production
  cancel-in-progress: false

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7

      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm

      - run: npm ci
      - run: npm test --if-present
      - run: npm run build

A few details worth copying:

  • workflow_dispatch adds a “Run workflow” button so you can redeploy without a new commit.
  • permissions: contents: read gives the workflow’s token the minimum access it needs.
  • concurrency makes sure two deploys never run at the same time and overwrite each other.
  • As of September 2026, actions/checkout and actions/setup-node are both on major version 7. Check the checkout releases and setup-node releases when you set this up.

Store credentials as secrets

Never put passwords, keys or hostnames with credentials in the workflow file. Add them under Settings → Secrets and variables → Actions and reference them as ${{ secrets.NAME }}. GitHub masks secret values in logs.

Typical secrets for a deploy:

Secret Used for
FTP_SERVER, FTP_USERNAME, FTP_PASSWORD FTP or FTPS deploys
SSH_HOST, SSH_USER, SSH_PORT SSH and rsync deploys
SSH_PRIVATE_KEY A deploy-only key, not your personal key
SSH_KNOWN_HOSTS The server’s host key, so the connection is verified

Create a dedicated user or FTP account for deploys, limited to the folder it needs to write to. If a secret leaks, you rotate one narrow credential instead of your main hosting password.

Deploy method 1: FTP or FTPS

FTP is the fallback when a host offers no SSH, which is common on shared hosting. The FTP Deploy Action keeps a state file on the server and only uploads files that changed:

      - name: Deploy over FTPS
        uses: SamKirkland/[email protected]
        with:
          server: ${{ secrets.FTP_SERVER }}
          username: ${{ secrets.FTP_USERNAME }}
          password: ${{ secrets.FTP_PASSWORD }}
          protocol: ftps
          local-dir: ./dist/
          server-dir: ./public_html/
          exclude: |
            **/.git*
            **/.git*/**
            **/node_modules/**

Use ftps rather than plain ftp whenever the host supports it, so credentials are not sent in clear text. For apps with a backend, make sure the exclude list protects files that must survive deploys, such as .env and upload folders.

Deploy method 2: rsync over SSH

When SSH is available, rsync is faster and more precise. It compares files and transfers only differences, and --delete removes files that no longer exist in the build:

      - name: Set up SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          echo "${{ secrets.SSH_KNOWN_HOSTS }}" > ~/.ssh/known_hosts

      - name: Deploy with rsync
        run: |
          rsync -az --delete \
            --exclude='.env' \
            --exclude='storage/' \
            -e "ssh -p ${{ secrets.SSH_PORT }}" \
            ./dist/ ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:~/public_html/

Get the value for SSH_KNOWN_HOSTS by running ssh-keyscan -p <port> <host> once from a trusted machine and checking the fingerprint. Skipping host key checking works, but it means you would not notice if someone intercepted the connection.

Deploy method 3: SSH commands on the server

For apps that need server-side steps (install dependencies, run migrations, clear caches), you can run a script on the server after the files arrive. For a Laravel app it might look like:

      - name: Run release steps
        run: |
          ssh -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} << 'EOF'
            set -e
            cd ~/laravel-app
            php artisan down
            git pull origin main
            composer install --no-dev --optimize-autoloader
            php artisan migrate --force
            php artisan optimize
            php artisan up
          EOF

set -e stops the script on the first failure, so a broken composer install does not continue into a migration. We go deeper into the Laravel-specific parts in how to deploy a Laravel app to cPanel.

Which method should you use?

Method Best for Pros Cons
FTP/FTPS Shared hosting without SSH Works almost everywhere Slower, cannot run commands
rsync over SSH Static sites and built apps Fast, handles deletes cleanly Needs SSH access
SSH commands Backend apps with migrations Full control of release steps More moving parts to secure
Platform deploy (Pages, Vercel, Netlify) Static and JAMstack sites Almost no setup Tied to that platform

Caching to speed up builds

Most build time goes into installing dependencies. actions/setup-node has a built-in cache option (shown above) for npm, pnpm and Yarn. For other tools, use actions/cache, which is on major version 6 as of September 2026:

      - name: Cache Composer packages
        uses: actions/cache@v6
        with:
          path: vendor
          key: composer-${{ hashFiles('composer.lock') }}
          restore-keys: composer-

Key the cache on the lockfile hash so it refreshes exactly when dependencies change. Do not cache build output you intend to deploy; always build fresh from the commit.

Environments and approvals

GitHub environments let you group secrets per target (for example staging and production) and add protection rules such as required reviewers, wait timers and branch restrictions. Reference one in the job:

jobs:
  deploy:
    runs-on: ubuntu-latest
    environment:
      name: production
      url: https://example.com

A common pattern is: every push to main deploys to staging automatically, and production needs a manual approval or a tagged release. Note that required reviewers on private repositories depend on your GitHub plan; on the Free, Pro and Team plans they are only available for public repositories.

Rollback: plan it before you need it

Every deploy method above can be rolled back, but only if you decide how in advance.

  1. Revert and redeploy. The simplest option: git revert the bad commit and push. The pipeline deploys the previous state. This works for most static sites.
  2. Redeploy an older run. Use workflow_dispatch with a ref input, or re-run an earlier successful workflow, to deploy a known-good commit.
  3. Release folders with a symlink. On servers with SSH, deploy each release into its own folder (releases/2026-07-08-1530) and point a current symlink at it. Rolling back is switching the symlink to the previous folder, which takes a second.
  4. Database migrations. Code rollbacks are easy; data rollbacks are not. Write migrations that are backwards compatible for at least one release, and back up the database before migrating.

Key takeaways

  • Workflow triggers on push to main plus workflow_dispatch
  • Minimal permissions and a concurrency group for deploys
  • Tests and build run before any upload
  • All credentials in GitHub secrets, using a deploy-only account or key
  • FTPS instead of FTP; rsync over SSH when available
  • Host key verified with known_hosts
  • .env and upload folders excluded from deploys
  • Dependency caching keyed on the lockfile
  • Separate staging and production environments
  • A written, tested rollback path, including the database

A good pipeline takes an afternoon to set up and saves hours every month after that, along with a lot of nervous Friday deploys. If you would like us to set one up for your site or app, or to fix a pipeline that keeps failing, see our deployment services or contact us with details of your hosting and stack.

Have a project in mind? Let’s talk.

Whether you run a business or a research group, tell us what you need built, fixed or evaluated. You get a free consultation and a clear written estimate — no obligation.

  • Free consultation
  • Written scope and estimate
  • We reply within one working day
Contact us