Appearance
04_GITHUB-CPANEL-DEPLOYMENT
Version: 3.0 Last Updated: February 7, 2026 Purpose: Automated deployment from GitHub to cPanel hosting via GitHub Actions and FTP
Overview
This guide documents deploying any application from a GitHub repository to cPanel hosting using GitHub Actions for CI/CD. When you push commits to your deployment branch, GitHub Actions automatically builds the application and deploys the output to cPanel via FTP.
Deployment Flow
Local Development → Git Push → GitHub Actions Build → FTP Deploy → Live on cPanelVersion Control
Your GitHub repository is your version control system. Every commit is tracked with full history — you can view changes, compare versions, and revert at any time. There is no need for cPanel's "Git Version Control" feature. cPanel simply receives the built files via FTP; GitHub handles everything else.
Why This Approach
- Framework-agnostic — works with any stack (React, Python, static HTML, etc.)
- No build tools needed on cPanel — the build runs on GitHub's servers
- Only built files reach production — source code stays in your repository
- No SSH keys or deploy keys required — FTP credentials stored securely as GitHub Secrets
- Automatic on every push — no manual steps after initial setup
- Full version history on GitHub — every change tracked, diffable, and revertible
Prerequisites
- A GitHub account
- A cPanel hosting account with FTP access
- Git installed locally
Initial Setup
1. Create a Private GitHub Repository
- Go to GitHub.com → New repository
- Set visibility to Private
- Initialize with a README or push existing code:
bash
git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/your-username/your-repo.git
git push -u origin master2. Get Your cPanel FTP Credentials
You need four values from your cPanel hosting:
| Value | Where to Find It | Example |
|---|---|---|
| FTP Server | cPanel → FTP Accounts or hosting welcome email | ftp.yourdomain.com |
| FTP Username | cPanel → FTP Accounts | user@yourdomain.com |
| FTP Password | Set in cPanel → FTP Accounts | (your password) |
| Remote Path | The directory on cPanel to deploy into | /public_html/your-app/ |
3. Store FTP Credentials as GitHub Secrets
GitHub Secrets keep your credentials encrypted and out of your code.
- Go to your GitHub repository
- Navigate to Settings → Secrets and variables → Actions
- Click New repository secret and add each:
| Secret Name | Value |
|---|---|
FTP_SERVER | Your FTP server hostname |
FTP_USERNAME | Your FTP username |
FTP_PASSWORD | Your FTP password |
4. Create the GitHub Actions Workflow
Create the workflow file in your repository at .github/workflows/deploy.yml:
yaml
name: Build and Deploy to cPanel
on:
push:
branches:
- master # Change to your deployment branch
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
# -----------------------------------------------
# BUILD STEP — customize for your framework
# -----------------------------------------------
# Example: Node.js / React / Vite
# - name: Set up Node.js
# uses: actions/setup-node@v4
# with:
# node-version: '20'
# - name: Install dependencies
# run: npm ci
# - name: Build
# run: npm run build
# Example: Python / Django
# - name: Set up Python
# uses: actions/setup-python@v5
# with:
# python-version: '3.12'
# - name: Install dependencies
# run: pip install -r requirements.txt
# - name: Collect static files
# run: python manage.py collectstatic --noinput
# Example: Static HTML (no build step needed)
# (skip straight to deploy)
# -----------------------------------------------
# DEPLOY STEP — pushes build output to cPanel
# -----------------------------------------------
- name: Deploy via FTP
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.FTP_SERVER }}
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: ./dist/ # Change to your build output folder
server-dir: /public_html/your-app/ # Change to your cPanel pathKey settings to customize:
| Setting | Description | Examples |
|---|---|---|
branches | Which branch triggers deployment | master, main, production |
| Build step | Uncomment and adjust for your framework | See examples in the workflow |
local-dir | Your build output folder (trailing slash required) | ./dist/, ./build/, ./public/, ./ |
server-dir | Destination path on cPanel (trailing slash required) | /public_html/, /public_html/my-app/ |
5. Push and Verify
bash
git add .github/workflows/deploy.yml
git commit -m "Add GitHub Actions deployment workflow"
git push origin masterThen check progress:
- Go to your GitHub repository → Actions tab
- Watch the workflow run
- Once complete, visit your live URL to verify
Day-to-Day Workflow
After initial setup, deployment is automatic:
bash
# 1. Make your changes locally
# 2. Stage and commit
git add .
git commit -m "Description of changes"
# 3. Push — deployment happens automatically
git push origin masterThat's it. GitHub Actions builds and deploys on every push.
Monitoring Deployments
- GitHub Actions tab — shows build status, logs, and errors for every push
- Green checkmark on a commit means deployment succeeded
- Red X means the build or deploy failed — click into the workflow run for details
Configuration
.gitignore
Keep credentials and unnecessary files out of your repository:
gitignore
# Environment / credentials
.env
.env.local
.env.production
# Dependencies
node_modules/
vendor/
venv/
__pycache__/
# Build output (built on CI, not committed)
dist/
build/
# OS files
.DS_Store
Thumbs.db
# IDE files
.vscode/
.idea/
# Logs
*.logExcluding Files from Deployment
The FTP action syncs your local-dir to server-dir. To exclude specific files from being uploaded, add an .ftp-deploy-sync-exclude file or use the exclude option in the workflow:
yaml
- name: Deploy via FTP
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.FTP_SERVER }}
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
local-dir: ./dist/
server-dir: /public_html/your-app/
exclude: |
**/.git*
**/.git*/**
**/node_modules/**
**/.envSPA Routing (Single Page Applications)
If you're deploying a single page application (React, Vue, etc.), add an .htaccess file in your build output so all routes resolve correctly:
apache
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]Place this file in your public/ folder (or wherever your build tool copies static assets from) so it gets included in the build output.
HTTPS Redirect
Force HTTPS on cPanel by adding to your .htaccess:
apache
RewriteEngine On
RewriteCond %{HTTPS} off
RewriteRule ^(.*)$ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]Database Migrations
Migration scripts live in your repository and are run manually on cPanel after deployment. This keeps your database changes version-controlled alongside your code.
Where to Store Migration Files
Keep migrations in a dedicated folder in your repository:
your-repo/
├── migrations/
│ ├── 001_create_users_table.sql
│ ├── 002_add_role_column.sql
│ └── 003_create_invoices_table.sql
├── src/
├── .github/workflows/
└── ...Or use your framework's built-in migration system:
| Framework | Migration Location | Run Command |
|---|---|---|
| Django | app/migrations/ (auto-generated) | python manage.py migrate |
| Laravel | database/migrations/ | php artisan migrate |
| Raw SQL | migrations/ (manual files) | mysql -u user -p dbname < migrations/001_file.sql |
Running Migrations After Deployment
- Log into cPanel → Terminal (or SSH into your server)
- Navigate to your application directory:
bash
cd ~/public_html/your-app- Run the migration:
bash
# Django
python manage.py migrate
# Laravel
php artisan migrate
# Raw SQL file
mysql -u dbuser -p dbname < migrations/001_create_users_table.sqlKeeping Track of Applied Migrations
Framework ORMs (Django, Laravel, etc.) handle this automatically — they maintain a migrations table in the database and only run new migrations.
Raw SQL migrations — track manually. Options:
- Naming convention — prefix with sequential numbers (
001_,002_, etc.) and keep a log of what's been applied - Migrations table — create a simple tracking table:
sql
CREATE TABLE IF NOT EXISTS applied_migrations (
filename VARCHAR(255) PRIMARY KEY,
applied_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);After running a migration:
sql
INSERT INTO applied_migrations (filename) VALUES ('001_create_users_table.sql');Check what's been applied:
sql
SELECT * FROM applied_migrations ORDER BY applied_at;Migration Best Practices
- Always test migrations locally first before running on production
- Never edit a migration that's already been applied — create a new one instead
- Back up the database before running migrations — cPanel → Backup Wizard or
mysqldump - One change per migration file — easier to track and rollback
- Include both up and down — if possible, write a rollback script for each migration
Rollback
Option 1: Re-run a Previous Deployment (Recommended)
- Go to GitHub → Actions tab
- Find the last successful workflow run
- Click Re-run all jobs
This rebuilds and deploys the code from that commit.
Option 2: Revert the Commit
bash
git revert HEAD
git push origin masterThis creates a new commit that undoes the last change, and triggers a fresh deployment.
Troubleshooting
Build Fails on GitHub Actions
| Symptom | Cause | Solution |
|---|---|---|
| Workflow never runs | Workflow file in wrong location | Must be at .github/workflows/deploy.yml |
| Build step fails | Missing dependencies or wrong versions | Check Node/Python version, verify package.json or requirements.txt |
| Build succeeds but output is empty | Wrong build command or output directory | Verify local-dir matches your actual build output folder |
FTP Deploy Fails
| Symptom | Cause | Solution |
|---|---|---|
| Authentication failed | Wrong FTP credentials | Verify secrets in GitHub → Settings → Secrets |
| Connection timeout | Wrong FTP server hostname | Check cPanel for correct FTP host |
| Permission denied | FTP user lacks write access to target directory | Check FTP account permissions in cPanel |
| Files deployed to wrong location | Wrong server-dir | Verify the path matches your cPanel directory structure |
Site Not Working After Deployment
| Symptom | Cause | Solution |
|---|---|---|
| 404 on all routes | SPA routing not configured | Add .htaccess rewrite rules (see SPA Routing section) |
| Old content showing | Browser cache | Hard refresh (Ctrl+Shift+R) |
| Blank page | Build output missing index.html | Check local-dir setting and build output |
| Mixed content warnings | HTTP resources on HTTPS page | Update asset URLs to use HTTPS or relative paths |
| 500 Internal Server Error | .htaccess syntax error | Check .htaccess file for typos |
Security Best Practices
- Keep the repository private — source code and workflow files are not publicly exposed
- Use GitHub Secrets for all credentials — never hardcode FTP passwords or API keys in workflow files
- Use
.gitignorefor environment files —.envfiles should never be committed - Force HTTPS — add the HTTPS redirect to
.htaccess - Set correct file permissions on cPanel — 644 for files, 755 for directories
Resources
Project Implementation: Business Manager App
This section documents the specific CI/CD setup for the Business Manager app (app.salesfield.net).
Architecture
| Component | Deployment Method | Details |
|---|---|---|
| React SPA (Vite) | Automated — GitHub Actions builds + FTP | Push to master → builds → FTP to cPanel |
| Django Backend | Manual — cPanel Terminal | Pull code + run update commands when backend changes |
Workflow File
Located at .github/workflows/deploy.yml. Single job:
deploy-frontend — Builds React SPA with Node 20, deploys dist/ contents via FTP to cPanel
GitHub Secrets Required
Configure at: Repository → Settings → Secrets and variables → Actions
Only 3 secrets needed (FTP credentials):
| Secret | Description |
|---|---|
FTP_SERVER | cPanel FTP hostname |
FTP_USERNAME | cPanel FTP username |
FTP_PASSWORD | cPanel FTP password |
Critical: .htaccess Protection
The FTP deploy excludes .htaccess from upload. This is essential because:
- Vite copies
public/.htaccess(SPA routing only) intodist/.htaccessduring build - The production server's
.htaccesscontains Passenger WSGI directives (PassengerEnabled On,PassengerAppType wsgi, etc.) that are NOT in the build.htaccess - If FTP overwrites the production
.htaccess, Django/Passenger stops working entirely
The production .htaccess is set up once during initial deployment (deploy.sh) and must not be touched by CI.
Server Directory Layout
/home/salesfield/
├── app-salesfield/ # Git repo clone
│ ├── django-backend/ # Django source
│ ├── src/ # React source
│ └── dist/ # React build (gitignored, built by CI)
│
├── public_html/app.salesfield.net/ # Subdomain document root
│ ├── index.html # React SPA entry
│ ├── assets/ # Vite JS/CSS chunks
│ ├── .htaccess # Passenger + SPA routing (DO NOT overwrite)
│ ├── passenger_wsgi.py # Symlink to django/passenger_wsgi.py
│ └── tmp/restart.txt # Passenger restart trigger
│
└── app-salesfield/django/ # Django runtime
├── venv/ # Python virtual environment
├── .env # Production environment variables
├── manage.py
├── config/ # Django settings
├── apps/ # Django apps
├── staticfiles/ # Collected static files
├── media/ # User uploads
├── logs/ # Error and cron logs
└── tmp/restart.txt # Passenger restart triggerDjango Backend Updates (via cPanel Terminal)
When you make changes to Django code (models, views, serializers, etc.), run these commands in cPanel → Terminal:
bash
cd ~/app-salesfield
git pull origin master
cp -r django-backend/* django/
cd ~/app-salesfield/django
source venv/bin/activate
pip install -r requirements.txt --quiet
python manage.py migrate --noinput
python manage.py collectstatic --noinput --verbosity=0
touch tmp/restart.txtFor quick reference, these steps are:
- Pull latest code from GitHub
- Copy updated Django files to the runtime directory
- Install any new Python dependencies
- Run database migrations
- Collect static files
- Restart Passenger
Implementation Phases
| Phase | Status | Description |
|---|---|---|
| 1. Workflow file created | DONE | .github/workflows/deploy.yml — frontend FTP deploy |
2. .gitignore updated | DONE | Added /dist — CI builds artifacts, not committed |
| 3. GitHub Secrets configured | TODO | Add 3 FTP secrets to repository settings |
| 4. Initial server setup | TODO | Run deploy.sh via cPanel Terminal for first-time provisioning |
| 5. First CI deployment | TODO | Push to master, verify workflow passes |
| 6. SSL setup | TODO | Let's Encrypt via cPanel for app.salesfield.net |
| 7. Cron jobs | TODO | Set up daily manifest and overdue invoice checks |
Monitoring
- GitHub Actions:
https://github.com/richardtheshannon/app.salesfield.net/actions - Re-run failed jobs: Click into the failed run → "Re-run failed jobs"
- Server logs: cPanel Terminal →
tail -f ~/app-salesfield/django/logs/error.log
Revision History
| Version | Date | Changes |
|---|---|---|
| 1.0 | 2026-02-02 | Initial documentation (cPanel Git Version Control approach) |
| 2.0 | 2026-02-07 | Complete rewrite: GitHub Actions + FTP deployment model, framework-agnostic |
| 2.1 | 2026-02-07 | Added Database Migrations section, version control clarification |
| 3.0 | 2026-02-07 | Added project-specific implementation section, frontend-only CI/CD, backend via cPanel Terminal |
End of Documentation