Appearance
GitHub to cPanel Deployment Guide
Version: 2.0 | Updated: February 2026 | Stack: Python/FastAPI + Vanilla JS + PostgreSQL
Overview
Automated deployment from GitHub to cPanel hosting via GitHub Actions and FTP. No SSH required.
Local Development → Git Push (main) → GitHub Actions → FTP Upload → cPanel → LiveWhat gets deployed:
- Backend:
main.py,config.py,models.py,requirements.txt(root-level Python files) - Frontend:
frontend/directory (HTML, CSS, JS, images, fonts) - cPanel restarts the Python app automatically via Passenger
Prerequisites
- GitHub repository with Actions enabled
- cPanel hosting account with:
- FTP access (credentials from cPanel → FTP Accounts)
- "Setup Python App" feature (CloudLinux/LiteSpeed)
- PostgreSQL database support
- File Manager and Terminal (web-based)
- Local development environment with Git installed
One-Time cPanel Setup
These steps are done once through the cPanel web interface. No SSH needed.
1. Create PostgreSQL Database
- Log into cPanel
- Go to PostgreSQL Databases (or MySQL Databases if PostgreSQL unavailable)
- Create a new database (e.g.,
username_appdb) - Create a database user with a strong password
- Add user to database with All Privileges
- Note the connection details:
| Field | Value |
|---|---|
| Host | localhost |
| Port | 5432 |
| Database | username_appdb |
| User | username_dbuser |
| Password | (your password) |
2. Setup Python App
- In cPanel, go to Setup Python App
- Click Create Application
- Configure:
| Setting | Value |
|---|---|
| Python version | 3.10+ (latest available) |
| Application root | public_html/app (your subdirectory) |
| Application URL | yourdomain.com/app |
| Application startup file | main.py |
| Application Entry point | app |
- Click Create
- Note the virtual environment path displayed (e.g.,
/home/username/virtualenv/public_html/app/3.10/)
3. Upload Initial Configuration
- Go to cPanel → File Manager
- Navigate to your application root (
public_html/app) - Create a new file:
.env - Paste your production environment configuration:
env
ENV=production
DATABASE_URL=postgresql://username_dbuser:yourpassword@localhost:5432/username_appdb
SECRET_KEY=generate-a-64-char-random-string-here
APP_NAME=MyApp
APP_VERSION=1.0.0
HOST=0.0.0.0
PORT=8668
LOG_LEVEL=INFOGenerate SECRET_KEY at any random string generator — use 64+ characters.
4. Install Dependencies (One-Time)
- In cPanel, go to Terminal (web-based terminal, not SSH)
- Activate the Python virtual environment:
bash
source /home/username/virtualenv/public_html/app/3.10/bin/activate- Install dependencies:
bash
cd ~/public_html/app
pip install -r requirements.txt- Verify the app starts:
bash
python main.pyPress Ctrl+C to stop. The app will run via Passenger in production.
5. Create Passenger WSGI File
In File Manager, create passenger_wsgi.py in your application root:
python
import sys
import os
# Add application directory to path
app_dir = os.path.dirname(os.path.abspath(__file__))
sys.path.insert(0, app_dir)
# Load environment variables
from dotenv import load_dotenv
load_dotenv(os.path.join(app_dir, '.env'))
# Import FastAPI app
from main import app as application6. Create Restart Directory
In File Manager, create a tmp directory inside your application root. Then create an empty file restart.txt inside it:
public_html/app/
├── tmp/
│ └── restart.txt ← Passenger watches this file
├── main.py
├── config.py
├── ...Passenger restarts the app whenever restart.txt is modified.
GitHub Secrets Setup
Store FTP credentials as encrypted secrets in your GitHub repository.
Add Secrets
- Go to your GitHub repository
- Navigate to Settings → Secrets and variables → Actions
- Click New repository secret for each:
| Secret Name | Value | Where to Find |
|---|---|---|
FTP_SERVER | ftp.yourdomain.com | cPanel → FTP Accounts |
FTP_USERNAME | deploy@yourdomain.com | cPanel → FTP Accounts |
FTP_PASSWORD | FTP account password | Set when creating FTP account |
Create Dedicated FTP Account (Recommended)
Instead of using your main cPanel FTP account:
- Go to cPanel → FTP Accounts
- Create a new account:
- Username:
deploy(becomesdeploy@yourdomain.com) - Password: strong generated password
- Directory:
/public_html/app(restrict to app directory)
- Username:
- Use these credentials in GitHub Secrets
GitHub Actions Workflow
Create the workflow file at .github/workflows/deploy.yml in your repository.
yaml
name: Deploy to cPanel
on:
push:
branches:
- main
jobs:
deploy:
name: FTP Deploy
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- 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 }}
server-dir: /public_html/app/
protocol: ftps
port: 21
exclude: |
**/.git*
**/.git*/**
.github/**
.claude/**
.claudeconfig/**
.claude-flow/**
.hive-mind/**
.swarm/**
.vscode/**
_TEMP/**
docs/**
scripts/**
templates/**
venv/**
__pycache__/**
node_modules/**
coordination/**
memory/**
docker-compose.yml
Dockerfile
Dockerfile.claude
nginx.conf
entrypoint.sh
deploy-production.bat
init-project.py
.env
.env.*
*.md
*.bat
*.ps1
*.sh
*.db
*.sqlite
- name: Restart Passenger App
uses: SamKirkland/FTP-Deploy-Action@v4.3.5
with:
server: ${{ secrets.FTP_SERVER }}
username: ${{ secrets.FTP_USERNAME }}
password: ${{ secrets.FTP_PASSWORD }}
server-dir: /public_html/app/tmp/
protocol: ftps
port: 21
local-dir: .github/restart/Restart Trigger File
Create .github/restart/restart.txt in your repository with any content (e.g., a single space). This file gets uploaded to tmp/restart.txt on every deploy, triggering Passenger to restart the Python app.
.github/
├── restart/
│ └── restart.txt ← Uploaded to tmp/ on each deploy
└── workflows/
└── deploy.yml ← GitHub Actions workflowConfiguration Notes
| Option | Value | Why |
|---|---|---|
protocol: ftps | FTPS (explicit TLS on port 21) | Encrypted transfer; most cPanel hosts support this |
server-dir | Must end with / | FTP-Deploy-Action requirement |
exclude | Dev-only files | Keeps production clean; .env excluded because it's managed on server |
If your host doesn't support FTPS, change protocol: ftps to protocol: ftp. Check cPanel → FTP Accounts → Configure FTP Client for supported protocols.
Files Deployed vs. Excluded
Deployed to Production
public_html/app/
├── main.py ← FastAPI application
├── config.py ← Configuration module
├── models.py ← Data models
├── requirements.txt ← Python dependencies
├── passenger_wsgi.py ← Passenger entry point
├── navigation.json ← Navigation config
├── frontend/
│ ├── index.html
│ ├── login.html
│ ├── components.html
│ ├── css/ ← All stylesheets
│ ├── js/ ← All JavaScript modules
│ ├── assets/ ← Fonts, images
│ └── layouts/ ← Layout demo pages
├── tmp/
│ └── restart.txt ← Passenger restart trigger
└── .env ← Production config (managed separately)Excluded from Deployment
| Path | Reason |
|---|---|
.claude/, .claude-flow/, .hive-mind/ | Dev tooling |
_TEMP/, docs/ | Documentation only |
venv/, __pycache__/ | Local Python artifacts |
scripts/, templates/ | Dev scripts |
docker-compose.yml, Dockerfile*, nginx.conf | Docker setup (not for cPanel) |
.env, .env.* | Credentials — managed via File Manager |
deploy-production.bat | Legacy manual deploy script |
*.md | README, CLAUDE.md, etc. |
Environment Configuration
Production .env (on cPanel)
Managed via cPanel File Manager. Never committed to Git.
When you need to update environment variables:
- Open cPanel → File Manager
- Navigate to
public_html/app/.env - Right-click → Edit
- Make changes, save
- Touch
tmp/restart.txt(edit and save it) to restart the app
Frontend API URL
The frontend config.js currently hardcodes API_BASE_URL to localhost:8668. For production, update frontend/js/config.js to detect the environment:
javascript
export const CONFIG = {
API_BASE_URL: window.location.hostname === 'localhost'
? 'http://127.0.0.1:8668'
: `${window.location.origin}/app`,
// ... rest of config
};CORS Origins
Update config.py CORS_ORIGINS to include your production domain:
python
CORS_ORIGINS = [
"http://localhost:3663",
"http://localhost:8668",
"http://127.0.0.1:3663",
"http://127.0.0.1:8668",
"https://yourdomain.com",
]Or load from .env:
python
CORS_ORIGINS = os.getenv("CORS_ORIGINS", "http://localhost:3663").split(",")Day-to-Day Deployment
Standard Workflow
bash
# 1. Make changes locally, test at localhost:3663
# 2. Stage and commit
git add main.py frontend/css/styles.css
git commit -m "Fix mobile layout overflow"
# 3. Push to main — GitHub Actions deploys automatically
git push origin main
# 4. Monitor deployment
# Go to GitHub → Actions tab → watch the workflow run
# 5. Verify live site
# Visit https://yourdomain.com/app
# Check https://yourdomain.com/app/healthWhen requirements.txt Changes
FTP can upload the file but can't run pip install. When you add/update Python dependencies:
- Push the updated
requirements.txt(auto-deployed via FTP) - Go to cPanel → Terminal
- Run:
bash
source /home/username/virtualenv/public_html/app/3.10/bin/activate
cd ~/public_html/app
pip install -r requirements.txt- Touch
tmp/restart.txtin File Manager to restart
Rollback
Method 1: Git Revert (Recommended)
bash
# Revert the last commit and push — GitHub Actions auto-deploys the revert
git revert HEAD
git push origin mainMethod 2: Re-run Previous Deployment
- Go to GitHub → Actions tab
- Find the last successful workflow run
- Click Re-run all jobs
Method 3: Manual Restore
- Download the previous version from GitHub (Code → Download ZIP from a specific commit)
- Upload files manually via cPanel → File Manager
- Touch
tmp/restart.txtto restart
Post-Deployment Checklist
After each deployment, verify:
- [ ] GitHub Actions workflow completed (green checkmark)
- [ ] Frontend loads:
https://yourdomain.com/app/frontend/ - [ ] Health endpoint responds:
https://yourdomain.com/app/health - [ ] API docs accessible:
https://yourdomain.com/app/docs - [ ] Login page renders correctly
- [ ] Theme toggle (light/dark) works
- [ ] Mobile layout displays properly
- [ ] No browser console errors
Troubleshooting
GitHub Actions Workflow Fails
| Symptom | Cause | Fix |
|---|---|---|
| "Connection refused" | Wrong FTP_SERVER secret | Check cPanel → FTP Accounts → Configure FTP Client for correct hostname |
| "Login incorrect" | Wrong FTP_USERNAME or FTP_PASSWORD | Verify credentials; try logging in with an FTP client like FileZilla first |
| "SSL handshake failed" | Host doesn't support FTPS | Change protocol: ftps to protocol: ftp in workflow |
| "Could not create directory" | FTP user lacks permissions | Ensure FTP account directory is set to public_html/app with write access |
Site Not Updating After Deploy
| Symptom | Cause | Fix |
|---|---|---|
| Old content showing | Browser cache | Hard refresh: Ctrl+Shift+R |
| Python changes not applied | Passenger not restarted | Edit tmp/restart.txt via File Manager and save |
| New files missing | File excluded by deploy config | Check the exclude list in deploy.yml |
| CSS not loading | Path issue | Verify CSS file paths use correct relative paths for subdirectory deployment |
Python App Errors
| Symptom | Cause | Fix |
|---|---|---|
| 500 Internal Server Error | Python exception | Check cPanel → Metrics → Errors for stack trace |
| "Module not found" | Missing dependency | Run pip install -r requirements.txt in cPanel Terminal |
| Database connection failed | Wrong credentials in .env | Verify DATABASE_URL in .env via File Manager |
| App not starting | Bad passenger_wsgi.py | Verify file exists and imports correctly; check error logs |
FTP Connection Issues
If FTPS doesn't work, try these protocols in order:
ftps(port 21) — explicit TLS, most secureftp(port 21) — unencrypted, works everywhere- Contact host support to confirm which FTP protocols are enabled
Revision History
| Version | Date | Changes |
|---|---|---|
| 2.0 | February 2026 | Full rewrite: GitHub Actions + FTP deployment, Python/FastAPI stack, no SSH |
| 1.0 | February 2026 | Initial documentation (PHP/MySQL, SSH-based — deprecated) |