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.

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_dispatchadds a “Run workflow” button so you can redeploy without a new commit.permissions: contents: readgives the workflow’s token the minimum access it needs.concurrencymakes sure two deploys never run at the same time and overwrite each other.- As of September 2026,
actions/checkoutandactions/setup-nodeare 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/FTP-Deploy-Action@v4.4.0
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.
- Revert and redeploy. The simplest option:
git revertthe bad commit and push. The pipeline deploys the previous state. This works for most static sites. - Redeploy an older run. Use
workflow_dispatchwith arefinput, or re-run an earlier successful workflow, to deploy a known-good commit. - Release folders with a symlink. On servers with SSH, deploy each release into its own folder (
releases/2026-07-08-1530) and point acurrentsymlink at it. Rolling back is switching the symlink to the previous folder, which takes a second. - 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
mainplusworkflow_dispatch - Minimal
permissionsand aconcurrencygroup 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 -
.envand 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.