Moving from "upload over FTP when I remember" to "every push to main deploys itself" is one of the best upgrades a small development team can make. This guide builds a GitHub Actions pipeline that tests your code, builds it on GitHub's own machines and ships the finished files to a cPanel or DirectAdmin shared hosting account, with an FTPS route for accounts without SSH.
Do all the heavy work, including composer install and npm run build, on the GitHub Actions runner, so the server only unpacks a finished release. Then copy one archive to your account over jailed SSH (off by default on Domain India shared hosting; ask support to enable it, and log in with a key, not a password) and unpack it into a new release folder. If you don't want SSH, deploy the built files over FTP with explicit TLS on port 21 instead.
1. What the pipeline does
When you push to main:
- Test.A fresh Ubuntu runner installs your dependencies and runs your test suite. If a test fails, nothing is deployed.
- Build.A second job installs production dependencies (
composer install --no-dev), builds frontend assets and packs the result into one archive. - Upload.The archive is copied to your hosting account over SSH with
scp. - Release.A short script on the server unpacks it into a new release folder, links your shared
.envandstorage, runs migrations and switches the site to the new release.
Rolling back means pointing the site at the previous release folder again.
2. Choose your deployment route
| Route | Needs | Good for | Limits |
|---|---|---|---|
| SSH + archive (this guide) | Jailed SSH enabled, an SSH key | Laravel and other PHP apps, sites with a build step | Needs a one-time SSH request to support |
| FTPS upload | Your FTP login only | Static sites, themes, simple PHP sites | No commands on the server, so no migrations |
| App Platform | A deploy token | Node.js apps and anything with a Dockerfile | Not for WordPress; no SSH |
Many tutorials, including older versions of this one, deploy with rsync over SSH. On our cPanel and DirectAdmin servers, rsync is not inside the jailed shell (checked 24 September 2026), so an rsync deploy fails. scp, SFTP, tar and Git are available, which is why this guide uploads one archive and unpacks it on the server.
3. One-time setup: SSH access and a deploy key
Jailed SSH access is available on every shared hosting plan (cPanel, DirectAdmin, Webuzo). It is off by default; ask support to enable it for your account through live chat or a ticket. Windows (Plesk) hosting has no SSH, so use the FTPS route in section 7 there.
Login is key-only: password authentication is switched off on our cPanel and DirectAdmin servers, and SSH uses port 22. Create a separate key just for GitHub, so you can remove it later without touching your own key:
ssh-keygen -t ed25519 -f ~/.ssh/github-deploy -C "github-actions-deploy" -N ""A deploy key has no passphrase, because CI can't type one. Protect it by keeping it only in GitHub Secrets.
- Authorise the public key.In cPanel, open SSH Access › Manage SSH Keys › Import Key, paste the contents of
github-deploy.pub, import it, then click Manage and Authorize. On DirectAdmin, add it as one line to~/.ssh/authorized_keys, or send the public key to support. - Test from your computer.Run
ssh -i ~/.ssh/github-deploy -p 22 USER@HOST 'echo ok'. Don't continue until this printsok. - Record the server's host key.Run
ssh-keyscan -p 22 HOSTand keep the output. GitHub will use it to check that it is talking to the real server. - Add GitHub secrets.In your repository, open Settings › Secrets and variables › Actions and add
SSH_HOST,SSH_USER,SSH_KEY(the whole private key file) andSSH_KNOWN_HOSTS(thessh-keyscanoutput).
Your username, server IP and panel address are in the client area: open My Hosting, click Manage and open the Access tab. Full SSH instructions are in Enabling and accessing jailed SSH.
4. Prepare the folders on the server
Over SSH, create a layout that keeps releases, shared files and the live pointer apart:
~/app/
releases/20260924-1030-a1b2c3d/
shared/.env
shared/storage/
current -> releases/20260924-1030-a1b2c3dPut your production .env in ~/app/shared/ by hand, once. It never goes through GitHub.
The website must be served from ~/app/current/public. For an addon domain or subdomain, you can set the document root when you create it in your control panel. For your main domain, ask support before you change anything, and never delete public_html yourself. Test the whole pipeline on a subdomain first.
5. The workflow file
Save this as .github/workflows/deploy.yml. It is written for a Laravel app; section 8 shows what to change for other stacks.
name: Deploy to shared hosting
on:
push:
branches: [main]
workflow_dispatch:
concurrency: production-deploy
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer
- run: composer install --prefer-dist --no-interaction --no-progress
- run: php artisan test
deploy:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
tools: composer
- run: composer install --no-dev --optimize-autoloader --no-interaction
- uses: actions/setup-node@v5
with:
node-version: '22'
cache: npm
- run: npm ci && npm run build
- name: Pack the release
run: tar --exclude-from=.deployignore -czf /tmp/release.tgz .
- name: Upload and activate
env:
SSH_KEY: ${{ secrets.SSH_KEY }}
SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
TARGET: ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}
run: |
install -m 700 -d ~/.ssh
printf '%s\n' "$SSH_KEY" > ~/.ssh/deploy && chmod 600 ~/.ssh/deploy
printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
RELEASE="$(date -u +%Y%m%d-%H%M)-${GITHUB_SHA::7}"
scp -i ~/.ssh/deploy -P 22 /tmp/release.tgz "$TARGET:app/release.tgz"
ssh -i ~/.ssh/deploy -p 22 "$TARGET" "bash -s" -- "$RELEASE" < deploy/activate.shSet php-version to the version your hosting account uses. You choose it in your control panel; check there for the versions available.
A .deployignore file in the project root keeps secrets and clutter out of the archive:
./.git
./.github
./node_modules
./tests
./.env
./storage6. The activation script
Commit this as deploy/activate.sh. It runs on the server, inside your jailed shell:
#!/bin/bash
set -euo pipefail
RELEASE="$1"
APP="$HOME/app"
NEW="$APP/releases/$RELEASE"
mkdir -p "$NEW"
tar -xzf "$APP/release.tgz" -C "$NEW"
rm -f "$APP/release.tgz"
ln -sfn "$APP/shared/.env" "$NEW/.env"
ln -sfn "$APP/shared/storage" "$NEW/storage"
cd "$NEW"
php artisan migrate --force
php artisan optimize
# switch the live pointer in one step
ln -sfn "$NEW" "$APP/current.tmp"
mv -T "$APP/current.tmp" "$APP/current"
# keep the five newest releases
cd "$APP/releases" && ls -1t | tail -n +6 | xargs -r rm -rfTo roll back, SSH in and point current at the previous folder the same way. Migrations are not undone by a rollback, so take a database export before any migration that changes an existing table.
PHP on the command line has the same disabled functions as your website, so a few artisan commands fail on the server; storage:link is one, which is why the script uses ln -s. Everything you run also counts towards your account's resource limits. See PHP disabled functions on shared hosting and Laravel on cPanel and DirectAdmin.
7. No SSH? Deploy over FTPS instead
For a static site, a theme or a simple PHP site, you can skip SSH and let an FTP deploy action upload the built files. Use FTP with explicit TLS on port 21, which works on every Domain India hosting account, and store the FTP login as secrets. Never use plain FTP.
- name: Upload over FTPS
uses: SamKirkland/[email protected]
with:
server: ${{ secrets.FTP_HOST }}
username: ${{ secrets.FTP_USER }}
password: ${{ secrets.FTP_PASSWORD }}
protocol: ftps
port: 21
local-dir: ./dist/
server-dir: public_html/Use the server hostname from your Access tab as FTP_HOST, so that the certificate name matches. Create a separate FTP account for CI in your control panel, limited to the folder it deploys to. This route cannot run migrations or other commands, so database changes are done by hand. See How to use FTP with SSL/TLS.
8. Other stacks
- Static sites and single-page apps: skip PHP entirely, run
npm run buildand deploy thedist/orbuild/folder, usually over FTPS. - WordPress: deploy your own theme or plugin folder, not the whole site. Content lives in the database, so don't overwrite
wp-content/uploads. - Node.js apps: a small app can run through Setup Node.js App on cPanel; see deploying a Node.js app on shared hosting. For anything larger, the App Platform runs it for you and deploys from CI with a deploy token.
9. Troubleshooting
| What you see | Likely cause | What to do |
|---|---|---|
| Permission denied (publickey) | Key not authorised, SSH not enabled, or the secret is incomplete | Authorise the key in cPanel, confirm with support that SSH is on, and paste the whole private key including its first and last lines |
| Host key verification failed | SSH_KNOWN_HOSTS is missing or out of date | Run ssh-keyscan again from your computer and update the secret |
| rsync: command not found | The workflow still uses rsync | Switch to the archive-and-scp method in section 5 |
| Connection timed out | Wrong host or port, or the runner's IP was blocked after failed logins | Use port 22 and the host from your Access tab; if it persists, ask support |
| Something "has been disabled for security reasons" | A PHP function disabled on shared hosting | Move that step to the CI runner, or change the command (see section 6) |
| The site still shows old content | The cPanel server's page cache | Add ?t=1 to the URL; cached pages can be kept for up to 120 minutes |
10. Where this fits on Domain India
Our cPanel and DirectAdmin shared hosting plans suit PHP applications that you build in CI and deploy as finished files, with jailed SSH enabled on request.
- 25 GB NVMe SSD Storage
- 50 GB Monthly Bandwidth
- 1 Website
- 10 Email Accounts
- 512 MB RAM per app
- 1.5 GB RAM total
- 2 vCPU
- 10 GB NVMe SSD
Prices on the cards are live and exclude 18% GST. If your app needs a long-running process, WebSockets, Redis or root access, a VPS is the better home; the same workflow idea applies there, with fewer limits.
Can I deploy to Domain India cPanel hosting from GitHub Actions?
Yes. Build and test on the GitHub Actions runner, then upload the finished files over jailed SSH or over FTP with explicit TLS on port 21. Jailed SSH is available on every shared hosting plan; it is off by default, so ask support to enable it for your account.
Can I run composer install on the server during a deploy?
You can, with composer.phar downloaded into your account; if it stops with a proc_open message, run it again with --no-scripts. Running composer install --no-dev on the GitHub Actions runner and uploading the project with its vendor folder is simpler and keeps build failures off the live site.
Why does my rsync deploy fail?
rsync is not available inside the jailed shell on our cPanel and DirectAdmin servers. Pack your build into a tar archive, copy it with scp and unpack it on the server, as shown in this guide.
Can GitHub Actions log in with my cPanel password?
No. SSH on our cPanel and DirectAdmin servers accepts key login only. Create a separate deploy key, authorise its public key on your account and store the private key as a GitHub secret.
Can I deploy without SSH?
Yes. Use an FTP deploy action with FTPS (explicit TLS) on port 21 and a dedicated FTP account. It uploads files but cannot run commands, so database migrations have to be done by hand.
How do I roll back a bad deployment?
With the release-folder layout in this guide, point the current link back at the previous release folder over SSH. Database migrations are not reversed, so export your database before migrations that change existing tables.
Does this work on Windows hosting?
Not over SSH, because Windows (Plesk) hosting has no SSH. Use the FTPS route to upload built files instead.
Ready to automate your deploys? Ask for jailed SSH through a support ticket, compare cPanel hosting and DirectAdmin hosting, or use the App Platform for container deploys from CI.
Open a ticket with your domain and say you need SSH for GitHub Actions deploys. Support switches on jailed SSH and confirms when it is ready.
Ask support to enable SSH