DevOps & CI/CD Pipelines

Deploying to cPanel from GitHub Actions: Automated CI/CD for Shared Hosting

By Domain India Team · DomainIndia SupportPublished 10 min read
Knowledge base article
Contents (10 sections)

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.

Key takeaways

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:

  1. Test.
    A fresh Ubuntu runner installs your dependencies and runs your test suite. If a test fails, nothing is deployed.
  2. Build.
    A second job installs production dependencies (composer install --no-dev), builds frontend assets and packs the result into one archive.
  3. Upload.
    The archive is copied to your hosting account over SSH with scp.
  4. Release.
    A short script on the server unpacks it into a new release folder, links your shared .env and storage, 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

RouteNeedsGood forLimits
SSH + archive (this guide)Jailed SSH enabled, an SSH keyLaravel and other PHP apps, sites with a build stepNeeds a one-time SSH request to support
FTPS uploadYour FTP login onlyStatic sites, themes, simple PHP sitesNo commands on the server, so no migrations
App PlatformA deploy tokenNode.js apps and anything with a DockerfileNot for WordPress; no SSH
rsync is not available inside the jail

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:

bash
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.

  1. 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.
  2. Test from your computer.
    Run ssh -i ~/.ssh/github-deploy -p 22 USER@HOST 'echo ok'. Don't continue until this prints ok.
  3. Record the server's host key.
    Run ssh-keyscan -p 22 HOST and keep the output. GitHub will use it to check that it is talking to the real server.
  4. 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) and SSH_KNOWN_HOSTS (the ssh-keyscan output).

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:

text
~/app/
  releases/20260924-1030-a1b2c3d/
  shared/.env
  shared/storage/
  current -> releases/20260924-1030-a1b2c3d

Put 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.

yaml
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.sh

Set 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:

text
./.git
./.github
./node_modules
./tests
./.env
./storage

6. The activation script

Commit this as deploy/activate.sh. It runs on the server, inside your jailed shell:

bash
#!/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 -rf

To 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.

Commands run under shared-hosting rules

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.

yaml
      - 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 build and deploy the dist/ or build/ 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 seeLikely causeWhat to do
Permission denied (publickey)Key not authorised, SSH not enabled, or the secret is incompleteAuthorise 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 failedSSH_KNOWN_HOSTS is missing or out of dateRun ssh-keyscan again from your computer and update the secret
rsync: command not foundThe workflow still uses rsyncSwitch to the archive-and-scp method in section 5
Connection timed outWrong host or port, or the runner's IP was blocked after failed loginsUse 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 hostingMove that step to the CI runner, or change the command (see section 6)
The site still shows old contentThe cPanel server's page cacheAdd ?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.

cPanel Starter
₹125/mo + GST
  • 25 GB NVMe SSD Storage
  • 50 GB Monthly Bandwidth
  • 1 Website
  • 10 Email Accounts
See plan details
App Developer
₹250/mo + GST
  • 512 MB RAM per app
  • 1.5 GB RAM total
  • 2 vCPU
  • 10 GB NVMe SSD
See plan details

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.

Ship every push without FTP

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

Was this article helpful?

Your answer helps us decide what to improve next.

Still need help? Open a support ticket and our team will reply.

Prefer an app? Add this site to your home screen.Get the app
Deploy to cPanel from GitHub Actions (2026) | Domain India