DEV Community

Cover image for Deploy Laravel and Vue to cPanel Automatically with GitHub Actions
Tahsin Abrar
Tahsin Abrar

Posted on

Deploy Laravel and Vue to cPanel Automatically with GitHub Actions

Automatically Deploy Laravel and Vue to cPanel with GitHub Actions

Uploading a ZIP file to cPanel after every small code change becomes frustrating very quickly.

You fix a bug, build the Vue frontend, create an archive, open cPanel File Manager, upload it, extract it, run migrations, clear Laravel caches, and finally check whether the application still works.

The code change may take two minutes. The deployment takes much longer.

A better approach is:

Push the code to GitHub and let GitHub Actions build and deploy the application to cPanel automatically.

After the initial setup, deployment becomes:

git add .
git commit -m "Fix checkout issue"
git push origin main
Enter fullscreen mode Exit fullscreen mode

GitHub Actions handles the remaining work.


The deployment architecture

This guide uses a push-based deployment:

diagram

The cPanel server does not clone or pull the private repository. Therefore, it does not need a GitHub deploy key, personal access token, or account-level GitHub SSH key.

GitHub Actions checks out its own repository with the workflow's built-in GITHUB_TOKEN. The workflow only needs a separate SSH key for this direction:

GitHub Actions → cPanel
Enter fullscreen mode Exit fullscreen mode

This keeps the setup simpler and avoids storing an unnecessary GitHub repository credential on the production server.


What the workflow will do

Every push to the main branch will:

  1. Check out the private repository.
  2. Install Laravel's production dependencies.
  3. Install frontend dependencies and build the Vue/Vite assets.
  4. Package the application without .env, storage, or server-managed files.
  5. Connect to cPanel through SSH.
  6. Upload and extract the new application files.
  7. Preserve the production .env, uploaded files, logs, and SSL challenge files.
  8. Run database migrations and Laravel optimization commands.
  9. Signal existing queue workers to restart.
  10. Check a public production URL when one is configured.

This is a practical deployment for small and medium applications. It is not a zero-downtime or automatic-rollback system.


Before you start

You need:

  • A Laravel application using Vue and Vite.
  • A private GitHub repository.
  • composer.lock and package-lock.json committed to the repository.
  • SSH access to the cPanel account.
  • A domain whose document root can point to Laravel's public directory.
  • The correct command-line PHP executable for the cPanel account.

Some shared-hosting providers disable SSH, symbolic links, long-running queue workers, or custom document roots. Confirm those features with your hosting provider before continuing.


Step 1: Prepare the application directory

Open:

cPanel → Advanced → Terminal
Enter fullscreen mode Exit fullscreen mode

Check the cPanel username and home directory:

whoami
echo "$HOME"
Enter fullscreen mode Exit fullscreen mode

Create the Laravel application directory:

mkdir -p "$HOME/example.com"
cd "$HOME/example.com"
pwd
Enter fullscreen mode Exit fullscreen mode

Example output:

/home/exampleuser/example.com
Enter fullscreen mode Exit fullscreen mode

Save this complete path. It will become the GitHub variable:

APP_DIR=/home/exampleuser/example.com
Enter fullscreen mode Exit fullscreen mode

Configure the domain document root as:

/home/exampleuser/example.com/public
Enter fullscreen mode Exit fullscreen mode

Do not point the domain to the Laravel project root. Only the public directory should be web-accessible.


Step 2: Create the production .env

The workflow will never upload .env from GitHub.

Create the production file directly on cPanel:

cd "$HOME/example.com"

umask 077
touch .env
chmod 600 .env

nano .env
Enter fullscreen mode Exit fullscreen mode

A minimal example looks like this:

APP_NAME="My Application"
APP_ENV=production
APP_KEY=base64:YOUR_APPLICATION_KEY
APP_DEBUG=false
APP_URL=https://example.com

LOG_CHANNEL=stack
LOG_LEVEL=error

DB_CONNECTION=mysql
DB_HOST=localhost
DB_PORT=3306
DB_DATABASE=exampleuser_database
DB_USERNAME=exampleuser_dbuser
DB_PASSWORD=YOUR_DATABASE_PASSWORD
Enter fullscreen mode Exit fullscreen mode

Add the mail, cache, session, queue, API, and other values required by your application.

Generate an application key from a trusted local copy of the project:

php artisan key:generate --show
Enter fullscreen mode Exit fullscreen mode

Copy the generated value into the production .env.

Never replace an existing production APP_KEY unless you understand the consequences. Changing it can invalidate encrypted cookies and make existing encrypted data unreadable.


Step 3: Find the correct PHP executable

In cPanel Terminal, run:

command -v php
php -v
Enter fullscreen mode Exit fullscreen mode

On some servers, the correct binary may look like:

/opt/cpanel/ea-php84/root/usr/bin/php
Enter fullscreen mode Exit fullscreen mode

Test the selected binary:

/opt/cpanel/ea-php84/root/usr/bin/php -v
Enter fullscreen mode Exit fullscreen mode

Save the working path as:

PHP_BIN=/opt/cpanel/ea-php84/root/usr/bin/php
Enter fullscreen mode Exit fullscreen mode

Use the PHP version required by your project. The PHP version used to build Composer dependencies in GitHub Actions should match the production server as closely as possible.


Step 4: Generate the deployment SSH key

Generate this key on your trusted local computer, not inside the repository and not on the cPanel server.

mkdir -p "$HOME/.ssh/deploy-keys"
chmod 700 "$HOME/.ssh/deploy-keys"
Enter fullscreen mode Exit fullscreen mode

Create an Ed25519 key pair:

ssh-keygen \
  -t ed25519 \
  -C "github-actions-to-cpanel" \
  -f "$HOME/.ssh/deploy-keys/cpanel_production" \
  -N ""
Enter fullscreen mode Exit fullscreen mode

This creates:

cpanel_production       ← Private key
cpanel_production.pub   ← Public key
Enter fullscreen mode Exit fullscreen mode

The key locations will be:

Private key → GitHub Actions secret
Public key  → cPanel authorized keys
Enter fullscreen mode Exit fullscreen mode

The private key must never be committed to the repository.


Step 5: Authorize the public key in cPanel

Display the public key locally:

cat "$HOME/.ssh/deploy-keys/cpanel_production.pub"
Enter fullscreen mode Exit fullscreen mode

Copy the complete line.

In cPanel, open:

Security
→ SSH Access
→ Manage SSH Keys
→ Import Key
Enter fullscreen mode Exit fullscreen mode

Paste the public key and import it. Then select:

Manage → Authorize
Enter fullscreen mode Exit fullscreen mode

Importing and authorizing may be separate actions in cPanel.

You can also add the public key from cPanel Terminal:

mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"

nano "$HOME/.ssh/authorized_keys"
chmod 600 "$HOME/.ssh/authorized_keys"
Enter fullscreen mode Exit fullscreen mode

Paste the public key on its own line.

Optional key restrictions

After the deployment works, you may restrict the key in authorized_keys:

no-agent-forwarding,no-port-forwarding,no-X11-forwarding,no-pty ssh-ed25519 AAAAC3... github-actions-to-cpanel
Enter fullscreen mode Exit fullscreen mode

These options disable common SSH features that the deployment does not need. Test the normal key first because hosting configurations vary.

Test the connection

From your local computer, run:

ssh \
  -i "$HOME/.ssh/deploy-keys/cpanel_production" \
  -p YOUR_SSH_PORT \
  YOUR_CPANEL_USER@YOUR_CPANEL_HOST
Enter fullscreen mode Exit fullscreen mode

Example:

ssh \
  -i "$HOME/.ssh/deploy-keys/cpanel_production" \
  -p 22 \
  exampleuser@server123.hosting-company.com
Enter fullscreen mode Exit fullscreen mode

Do not continue until this connection works.


Step 6: Verify and save the SSH host key

Do not make the workflow trust whatever SSH key it receives during deployment.

From a trusted computer, collect the server's public host keys:

ssh-keyscan \
  -p YOUR_SSH_PORT \
  YOUR_CPANEL_HOST > cpanel_known_hosts
Enter fullscreen mode Exit fullscreen mode

Display their fingerprints:

ssh-keygen \
  -lf cpanel_known_hosts \
  -E sha256
Enter fullscreen mode Exit fullscreen mode

Verify the fingerprint with your hosting provider or hosting control panel. After verification, copy the complete contents of cpanel_known_hosts.

If the hosting provider later changes the server's SSH host key, the deployment will fail until this value is updated. That failure is safer than silently trusting an unexpected server.


Step 7: Add GitHub secrets and variables

Open the repository:

Settings
→ Secrets and variables
→ Actions
Enter fullscreen mode Exit fullscreen mode

Repository secrets

Add these secrets:

Secret Value
CPANEL_SSH_PRIVATE_KEY Complete private deployment key
CPANEL_KNOWN_HOSTS Verified contents of cpanel_known_hosts

Display the private key locally:

cat "$HOME/.ssh/deploy-keys/cpanel_production"
Enter fullscreen mode Exit fullscreen mode

Copy the complete value, including:

-----BEGIN OPENSSH PRIVATE KEY-----
...
-----END OPENSSH PRIVATE KEY-----
Enter fullscreen mode Exit fullscreen mode

Do not paste the .pub key into CPANEL_SSH_PRIVATE_KEY.

CPANEL_KNOWN_HOSTS is not confidential, but storing the multiline value as a secret is convenient and prevents accidental editing in the workflow file.

Repository variables

Add these variables:

Variable Example
APP_DIR /home/exampleuser/example.com
CPANEL_HOST server123.hosting-company.com
CPANEL_PORT 22
CPANEL_USER exampleuser
PHP_BIN /opt/cpanel/ea-php84/root/usr/bin/php
APP_URL https://example.com
HEALTHCHECK_URL https://example.com/up

APP_URL is the public application URL used during the frontend build. HEALTHCHECK_URL is optional; a lightweight public health endpoint is better than a page that requires authentication.

Optional production environment

On GitHub plans that support environments for private repositories, you can move the same secrets and variables into a protected environment named production. Then add this line to the deployment job:

environment: production
Enter fullscreen mode Exit fullscreen mode

A protected environment can add approvals and restrict which branches may deploy. If your plan does not support those features for a private repository, repository-level secrets and variables still work.


Step 8: Add the GitHub Actions workflow

Create:

.github/workflows/deploy-cpanel.yml
Enter fullscreen mode Exit fullscreen mode

Add the following workflow:

name: Deploy Laravel and Vue to cPanel

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

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

env:
  APP_DIR: ${{ vars.APP_DIR }}
  CPANEL_HOST: ${{ vars.CPANEL_HOST }}
  CPANEL_PORT: ${{ vars.CPANEL_PORT || '22' }}
  CPANEL_USER: ${{ vars.CPANEL_USER }}
  PHP_BIN: ${{ vars.PHP_BIN }}
  APP_URL: ${{ vars.APP_URL }}
  HEALTHCHECK_URL: ${{ vars.HEALTHCHECK_URL }}
  NODE_VERSION: "24"
  NODE_OPTIONS: --max-old-space-size=4096

jobs:
  build-and-deploy:
    name: Build and deploy production
    runs-on: ubuntu-latest
    timeout-minutes: 35

    steps:
      - name: Checkout repository
        uses: actions/checkout@v7
        with:
          fetch-depth: 1
          persist-credentials: false

      - name: Set up PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: "8.4"
          coverage: none
          tools: composer:v2
          extensions: mbstring, xml, ctype, iconv, intl, pdo_mysql, bcmath, curl, fileinfo, gd, openssl, zip

      - name: Install production PHP dependencies
        env:
          COMPOSER_ALLOW_SUPERUSER: "1"
        run: |
          composer validate --no-check-publish

          composer install \
            --no-dev \
            --prefer-dist \
            --no-interaction \
            --no-progress \
            --optimize-autoloader

      - name: Set up Node.js
        uses: actions/setup-node@v7
        with:
          node-version: ${{ env.NODE_VERSION }}
          cache: npm
          cache-dependency-path: package-lock.json

      - name: Install frontend dependencies
        run: npm ci --no-audit --no-fund

      - name: Build frontend assets
        env:
          VITE_APP_URL: ${{ vars.APP_URL }}
        run: npm run build

      - name: Verify the build
        run: |
          test -f artisan
          test -f vendor/autoload.php
          test -d public/build
          test -f public/build/manifest.json

      - name: Validate deployment configuration
        env:
          SSH_PRIVATE_KEY: ${{ secrets.CPANEL_SSH_PRIVATE_KEY }}
          SSH_KNOWN_HOSTS: ${{ secrets.CPANEL_KNOWN_HOSTS }}
        run: |
          set -Eeuo pipefail

          : "${APP_DIR:?Missing APP_DIR variable}"
          : "${CPANEL_HOST:?Missing CPANEL_HOST variable}"
          : "${CPANEL_PORT:?Missing CPANEL_PORT variable}"
          : "${CPANEL_USER:?Missing CPANEL_USER variable}"
          : "${PHP_BIN:?Missing PHP_BIN variable}"
          : "${SSH_PRIVATE_KEY:?Missing CPANEL_SSH_PRIVATE_KEY secret}"
          : "${SSH_KNOWN_HOSTS:?Missing CPANEL_KNOWN_HOSTS secret}"

      - name: Configure SSH
        env:
          SSH_PRIVATE_KEY: ${{ secrets.CPANEL_SSH_PRIVATE_KEY }}
          SSH_KNOWN_HOSTS: ${{ secrets.CPANEL_KNOWN_HOSTS }}
        run: |
          install -m 700 -d "$HOME/.ssh"

          printf '%s\n' "$SSH_PRIVATE_KEY" > "$HOME/.ssh/id_ed25519"
          chmod 600 "$HOME/.ssh/id_ed25519"

          printf '%s\n' "$SSH_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
          chmod 600 "$HOME/.ssh/known_hosts"

      - name: Package the application
        run: |
          tar -czf /tmp/app-deploy.tar.gz \
            --exclude='./.git' \
            --exclude='./.github' \
            --exclude='./node_modules' \
            --exclude='./.env' \
            --exclude='./.env.*' \
            --exclude='./storage' \
            --exclude='./public/storage' \
            --exclude='./public/hot' \
            --exclude='./public/.well-known' \
            --exclude='./.user.ini' \
            --exclude='./php.ini' \
            --exclude='./tests' \
            .

      - name: Deploy to cPanel
        run: |
          set -Eeuo pipefail

          SSH=(
            ssh
            -i "$HOME/.ssh/id_ed25519"
            -p "$CPANEL_PORT"
            -o BatchMode=yes
            -o ConnectTimeout=15
            -o IdentitiesOnly=yes
            -o StrictHostKeyChecking=yes
            -o UserKnownHostsFile="$HOME/.ssh/known_hosts"
            "$CPANEL_USER@$CPANEL_HOST"
          )

          SCP=(
            scp
            -i "$HOME/.ssh/id_ed25519"
            -P "$CPANEL_PORT"
            -o BatchMode=yes
            -o ConnectTimeout=15
            -o IdentitiesOnly=yes
            -o StrictHostKeyChecking=yes
            -o UserKnownHostsFile="$HOME/.ssh/known_hosts"
          )

          "${SSH[@]}" \
            "mkdir -p '$APP_DIR' && test -f '$APP_DIR/.env' && test -x '$PHP_BIN'" || {
              echo '::error::APP_DIR, production .env, or PHP_BIN is invalid.'
              exit 1
            }

          "${SSH[@]}" \
            "cd '$APP_DIR' && if [ -f artisan ]; then '$PHP_BIN' artisan down --retry=60 || true; fi"

          bring_application_up() {
            "${SSH[@]}" \
              "cd '$APP_DIR' && if [ -f artisan ]; then '$PHP_BIN' artisan up; fi"
          }

          remote_update_started=0

          handle_failure() {
            status=$?

            if [ "$status" -ne 0 ]; then
              if [ "$remote_update_started" -eq 0 ]; then
                bring_application_up || true
              else
                echo '::warning::Deployment failed after the remote update started.'
                echo '::warning::The application may remain in maintenance mode for inspection.'
              fi
            fi
          }

          trap handle_failure EXIT

          "${SCP[@]}" \
            /tmp/app-deploy.tar.gz \
            "$CPANEL_USER@$CPANEL_HOST:$APP_DIR/app-deploy.tar.gz"

          remote_update_started=1

          "${SSH[@]}" \
            "APP_DIR='$APP_DIR' PHP_BIN='$PHP_BIN' bash -se" <<'REMOTE'
          set -Eeuo pipefail

          cd "$APP_DIR"

          test -f app-deploy.tar.gz

          tar -xzf app-deploy.tar.gz --overwrite
          rm app-deploy.tar.gz

          test -f .env
          test -f artisan
          test -f vendor/autoload.php
          test -f public/build/manifest.json

          mkdir -p \
            storage/app/public \
            storage/framework/cache/data \
            storage/framework/sessions \
            storage/framework/views \
            storage/logs \
            bootstrap/cache

          chmod -R ug+rwX storage bootstrap/cache

          "$PHP_BIN" artisan package:discover --ansi
          "$PHP_BIN" artisan config:clear
          "$PHP_BIN" artisan route:clear
          "$PHP_BIN" artisan view:clear
          "$PHP_BIN" artisan event:clear || true
          "$PHP_BIN" artisan migrate --force

          if [ ! -L public/storage ]; then
            "$PHP_BIN" artisan storage:link
          fi

          "$PHP_BIN" artisan optimize
          "$PHP_BIN" artisan queue:restart || true

          chmod -R ug+rwX storage bootstrap/cache
          REMOTE

          bring_application_up
          trap - EXIT

      - name: Check the production URL
        if: ${{ vars.HEALTHCHECK_URL != '' }}
        run: |
          curl \
            --fail \
            --silent \
            --show-error \
            --location \
            --retry 5 \
            --retry-delay 5 \
            --retry-connrefused \
            --max-time 30 \
            "$HEALTHCHECK_URL" > /dev/null

      - name: Write deployment summary
        run: |
          {
            echo '## Deployment completed'
            echo
            echo "- Directory: \`$APP_DIR\`"
            echo '- Composer dependencies built on GitHub Actions'
            echo '- Vue and Vite assets built on GitHub Actions'
            echo '- Production .env and storage preserved'
          } >> "$GITHUB_STEP_SUMMARY"
Enter fullscreen mode Exit fullscreen mode

The example uses PHP 8.4 and Node.js 24. Change both versions to match your application and production server.

The workflow gives GITHUB_TOKEN only read access to repository contents. It also disables persisted checkout credentials because the remaining steps do not need authenticated Git commands.


Step 9: Run the first deployment

Commit the workflow:

git add .github/workflows/deploy-cpanel.yml
git commit -m "Add automatic cPanel deployment"
git push origin main
Enter fullscreen mode Exit fullscreen mode

Open:

GitHub repository → Actions
Enter fullscreen mode Exit fullscreen mode

Select the workflow and follow the logs.

After it finishes, verify the application from cPanel:

cd "$HOME/example.com"

PHP_BIN=/opt/cpanel/ea-php84/root/usr/bin/php

"$PHP_BIN" artisan about
"$PHP_BIN" artisan migrate:status

ls -la public/build
ls -la public/storage
Enter fullscreen mode Exit fullscreen mode

Then open the production URL.


What the workflow preserves

The deployment archive excludes:

.env
.env.*
storage/
public/storage/
public/.well-known/
.user.ini
php.ini
Enter fullscreen mode Exit fullscreen mode

This prevents the workflow from replacing:

  • Production credentials and API keys.
  • User-uploaded files.
  • Application logs and sessions.
  • The public storage symbolic link.
  • Server-managed SSL challenge files.
  • Hosting-specific PHP configuration files.

The workflow includes Composer's vendor directory and Vite's public/build directory because both are built on GitHub Actions before deployment.


Important security notes

Do not add a GitHub deploy key to cPanel for this workflow

The server does not run git pull. GitHub Actions checks out the repository and sends a prepared archive to cPanel.

Adding a cPanel-to-GitHub key would create an unnecessary credential on the production server. If the server were compromised, that credential could allow the attacker to clone the repository it belongs to.

Never put secrets in VITE_*

Values prefixed with VITE_ may become part of the browser's JavaScript bundle.

Safe examples:

VITE_APP_NAME
VITE_APP_URL
Enter fullscreen mode Exit fullscreen mode

Unsafe examples:

VITE_DB_PASSWORD
VITE_STRIPE_SECRET
VITE_OPENAI_API_KEY
Enter fullscreen mode Exit fullscreen mode

Only put public frontend configuration in VITE_* variables.

Protect the production workflow

A person who can modify a production workflow may try to use the secrets available to that workflow.

At minimum:

  • Protect the main branch.
  • Require pull requests and reviews.
  • Require review for changes inside .github/workflows/.
  • Consider a CODEOWNERS rule for workflow files.
  • Use a protected production environment when your GitHub plan supports it.
  • Keep permissions: contents: read unless more access is genuinely required.
  • Review third-party actions and consider pinning them to full commit SHAs.
  • Use Dependabot to keep action versions updated.

Back up before risky migrations

The workflow runs:

php artisan migrate --force
Enter fullscreen mode Exit fullscreen mode

A code rollback does not restore deleted or modified database data. Back up the database before destructive or difficult-to-reverse migrations.

queue:restart does not start a queue worker

The command only tells existing workers to exit gracefully after finishing their current job. A process manager, hosting feature, or scheduled command must already be running the workers.

On shared hosting, you may need a cPanel cron job instead of a continuously running worker.


Common errors

Permission denied (publickey)

Check that:

  • The deployment public key was imported and authorized in cPanel.
  • The matching private key was saved in CPANEL_SSH_PRIVATE_KEY.
  • CPANEL_USER, CPANEL_HOST, and CPANEL_PORT are correct.
  • The hosting account allows SSH access.

Test locally:

ssh \
  -vvv \
  -i "$HOME/.ssh/deploy-keys/cpanel_production" \
  -p YOUR_SSH_PORT \
  YOUR_CPANEL_USER@YOUR_CPANEL_HOST
Enter fullscreen mode Exit fullscreen mode

Host key verification failed

The value in CPANEL_KNOWN_HOSTS does not match the server.

Do not disable strict host checking. Confirm whether the hosting provider intentionally changed the SSH host key, verify the new fingerprint, and then update the secret.

Production .env is missing

Create .env inside APP_DIR, not inside public:

cd /home/YOUR_CPANEL_USER/YOUR_APP_DIRECTORY

touch .env
chmod 600 .env
nano .env
Enter fullscreen mode Exit fullscreen mode

public/build/manifest.json is missing

Run locally:

npm ci
npm run build
Enter fullscreen mode Exit fullscreen mode

Confirm that Vite creates:

public/build/manifest.json
Enter fullscreen mode Exit fullscreen mode

If your project uses a different Vite output directory, update the workflow checks.

Laravel returns HTTP 500

Check the log:

tail -n 100 storage/logs/laravel.log
Enter fullscreen mode Exit fullscreen mode

Then inspect the environment, permissions, PHP extensions, database connection, and cached configuration.

If a failed deployment left the application in maintenance mode, bring it back only after fixing the problem:

"$PHP_BIN" artisan up
Enter fullscreen mode Exit fullscreen mode

storage:link fails

Check whether public/storage already exists as a normal directory or whether the hosting provider blocks symbolic links:

ls -la public/storage
Enter fullscreen mode Exit fullscreen mode

Remove or rename an incorrect directory only after confirming it does not contain files that must be preserved.


A note about deleted files

This workflow extracts the new archive over the existing application.

Files with matching names are replaced, but a file deleted from Git may remain on the server. This is one limitation of the simple archive-based approach.

For many small applications, that trade-off is acceptable. When the application grows, move to release directories and a current symbolic link:

releases/
├── 20260730-120001/
├── 20260730-123500/
└── 20260730-130800/

current -> releases/20260730-130800
Enter fullscreen mode Exit fullscreen mode

That architecture makes atomic switching, cleanup, and code rollback easier, but it requires a more advanced deployment workflow and a shared storage directory.


Conclusion

Manual cPanel deployment works, but it becomes slow and error-prone when deployments are frequent.

With this setup, the regular process becomes:

git add .
git commit -m "Describe the change"
git push origin main
Enter fullscreen mode Exit fullscreen mode

GitHub Actions will:

  • Check out the private repository with its built-in token.
  • Install Laravel's production dependencies.
  • Build the Vue and Vite assets.
  • Upload the application to cPanel through SSH.
  • Preserve .env, storage, uploads, logs, and server-managed files.
  • Run migrations and optimize Laravel.
  • Signal existing queue workers to restart.
  • Check the production website.

Most importantly, the cPanel server does not need access to the private GitHub repository. It only receives the deployment created by GitHub Actions.

The first setup requires care around SSH keys, host verification, server paths, PHP versions, and production secrets. After that, deployments become repeatable, predictable, and much less stressful.

Top comments (0)