# Python App Hosting

WHost lets you host Python web applications — Django, Flask, FastAPI and similar — behind Gunicorn/Uvicorn application servers and an Nginx reverse proxy.

Python hosting is the free **Python Hosting** plugin: activate it once under **Settings → Plugins**. While it is inactive, the **Python Apps** menu entry is hidden in both panels and every Python app endpoint answers `503 PLUGIN_DISABLED`.

---

## Supported Stack

| Component | Options |
|-----------|---------|
| Framework | Django, Flask, FastAPI, Custom |
| Application Server | Gunicorn (WSGI; ASGI with the Uvicorn worker class), Uvicorn (ASGI) |
| Python Versions | Whichever of 3.10 – 3.13 are installed on the server (`/usr/bin/python3.X`) |
| Web Server | Nginx reverse proxy, written automatically — the server's web server setup must include Nginx (Nginx or Nginx + Apache) |
| Process Management | systemd (auto-restart, the account's cgroup slice) |

---

## Admin Panel

### Creating an Application

**Tools → Python Apps → New Python App** opens a chooser:

- **Smart Deploy** (recommended) — upload an archive or point to a path on the server; the project is analyzed and deployed in one run. See [Smart Deploy](#smart-deploy).
- **Create Blank App** — creates the runtime scaffold only, with the form below; you add the code and run the setup steps yourself.

| Field | Description | Example |
|-------|-------------|---------|
| Account | Account the application will belong to | `siteowner` |
| App Name | Unique identifier: starts with a lowercase letter, then lowercase letters, digits, `_` or `-` (2–31 characters) | `turkuaz` |
| Display Name | Human-readable name | `Turkuaz Site Management` |
| Domain | A domain of the account — its primary domain, an addon domain or a subdomain (picking the account fills in its primary domain) | `turkuaz.example.com` |
| Python Version | One of the versions installed on the server | `3.12` |
| Framework | Application type | `Django` |
| App Server | Gunicorn (WSGI) or Uvicorn (ASGI) | `Gunicorn (WSGI)` |
| Entry Point | WSGI/ASGI entry point (`module.path:callable`) | `turkuaz.wsgi:application` |
| Workers | Worker count (1–16, and no more than the account's Python Workers / App limit) | `2` |
| Worker Class | Sync, Threaded (gthread) or Uvicorn Worker (ASGI) — used by Gunicorn | `Sync` |
| WebSocket Support | Enable for ASGI applications | Off |
| Auto Restart | Automatically restart on crash | On |

During creation the system automatically:
- Creates the application directory `/home/{user}/{app}/` and a virtualenv
- Installs Gunicorn (for Uvicorn apps: `uvicorn[standard]` and Gunicorn)
- Defines a systemd service and a logrotate rule
- Configures the Nginx reverse proxy
- Starts the service — until the code is in place it fails to boot (see [Typical Setup Steps](#typical-setup-steps))

### Managing an Application

From the `⋮` menu on the table row, or the detail screen opened by clicking the application name:

**Row menu:**
- Edit App (display name, domain, entry point, workers, worker class, WebSocket support, auto restart)
- Packages / Environment Variables / Logs / manage.py — open the detail screen on that tab
- Start (when not running) / Stop (when running) / Restart
- Delete App (requires confirmation) — removes the service, the Nginx configuration, the virtualenv, the application directory `/home/{user}/{app}/` with its code, and the app's log files

**Detail screen (5 tabs):**

| Tab | Contents |
|-----|----------|
| Overview | Full configuration and status |
| Packages | Installed pip packages (name, version) — read-only in the admin panel; installing, uninstalling and requirements.txt are in the client panel and the API |
| Environment Variables | Edit environment variables (KEY=VALUE) |
| Logs | View error and access logs |
| manage.py | Run Django commands |

### All Applications View

The Python Apps page in the admin panel lists every application across all accounts in a single table. Narrow the view with search (app name, display name, account, domain), the status filter (Running / Stopped / Failed), or the Python version filter (shown when the apps use more than one version).

---

## Client Panel

### Creating an Application

**Tools → Python Apps → Create App** opens the same chooser (**Smart Deploy** or **Create Blank App**). The blank-app form matches the admin form without the Account selector — the application is created under the signed-in user's account — and without Worker Class (change it later with Edit App).

### Managing an Application

The client panel has the same row menu and the same five detail tabs. Differences from the admin panel:

- The **Packages** tab can also install and uninstall packages and install `requirements.txt` (see [Pip Package Management](#pip-package-management)).
- The **manage.py** list also offers `createsuperuser` and `shell` (see [Django Commands](#django-commands-manage-py)).
- The list has a search box (app name, display name, domain) but no status or version filter.

---

## Smart Deploy

Available in both panels from the chooser:

1. **Source** — **Upload Archive** (`.zip` / `.tar.gz`, up to 250 MB) or **Server Path** (a folder or an archive inside the account's home directory; use SSH/SFTP and this mode for larger projects).
2. **Analyze** — detects the framework, app server, Python version, entry point, static paths and the environment variables the code reads, and names missing system libraries.
3. **Deploy** — review the detected values and the options (**Overwrite Existing**, **Include .env Files**, **Run migrate**, **Run collectstatic**), then deploy. The pipeline copies the files to `/home/{user}/{app}/`, creates the app, installs `requirements.txt`, runs `migrate` and `collectstatic` for Django when selected, and starts the app; the dialog streams the log live.

Leaving `DB_NAME`, `DB_USER` and `DB_PASSWORD` empty (for a project that reads them) creates a MySQL database and user during the deploy and passes their credentials to the app. A database dump or media folder inside the uploaded project is detected in both panels; uploading a separate SQL dump or media archive works in the admin panel only.

---

## Pip Package Management

Detail screen → **Packages** tab. The admin panel lists the installed packages; installing and removing them is done in the client panel (or through the API).

| Action | Description |
|--------|-------------|
| Install a package | Type the package name and click **Install** (multiple: separate with spaces) |
| requirements.txt | **Install from requirements.txt** installs `/home/{user}/{app}/requirements.txt` |
| Remove a package | Trash icon at the end of the row (**Uninstall**) |

Accepted package name formats: `django`, `django==6.0.1`, `gunicorn>=22.0`, `uvicorn[standard]`

---

## Environment Variables

Detail screen → **Environment Variables** tab

| Action | Description |
|--------|-------------|
| Add | Enter Key and Value, click **Add Variable** |
| Edit | Change the value inline on the row |
| Remove | × icon at the end of the row |
| Save | **Save Variables** — additions, edits and removals take effect only when saved |

Keys use letters, digits and `_` and do not start with a digit; values are single-line, up to 16,384 characters. After saving, the application is automatically restarted if it is running.

The variables are written to `/home/{user}/{app}/.env` (0600) and handed to the application's service; a variable with an empty value is left out. `manage.py` commands — from the tab, the API or Smart Deploy — run outside that service and see these variables only when the project reads `.env` itself (for example with python-decouple or django-environ).

**Common variables:**

```
DJANGO_SETTINGS_MODULE = myapp.settings.production
SECRET_KEY             = your-secure-random-key
DATABASE_URL           = mysql://user:pass@localhost/dbname
DEBUG                  = false
ALLOWED_HOSTS          = example.com
```

---

## Viewing Logs

Detail screen → **Logs** tab

- **Error Log:** application errors and tracebacks
- **Access Log:** HTTP requests (IP, path, status code)
- Pick the log type from the dropdown; click **Refresh** to reload. The tab shows the last 200 lines.

Log files: `/home/{user}/logs/{app}-error.log` and `{app}-access.log`

---

## Django Commands (manage.py)

Detail screen → **manage.py** tab (Django projects — the application directory must contain `manage.py`)

Pick a command from the dropdown and click **Run**. The command runs as the account's user in the application directory, with a 5-minute limit; the exit code, output and errors are shown on screen.

| Command | Description |
|---------|-------------|
| `migrate` | Apply database migrations (runs with `--noinput`) |
| `collectstatic` | Collect static files (runs with `--noinput`) |
| `showmigrations` | Show migration status |
| `check` | Check the project for issues |

The client panel's list also shows `createsuperuser` and `shell`, and the API accepts both, but they need an interactive terminal, which a panel or API run does not provide. Create the Django admin user over SSH instead, when the account has shell access: `cd ~/{app} && ~/venvs/{app}/bin/python manage.py createsuperuser`.

---

## Typical Setup Steps

To host a new Django application as a blank app (**Smart Deploy** covers steps 4–9 in one run):

1. **Create an account** — Accounts → Create Account
2. **Add a domain** (only if the app uses a domain other than the account's primary domain) — Domains → Add Addon Domain or Add Subdomain
3. **Create a database** — Databases → Create Database
4. **Create the Python application** — Python Apps → New Python App → Create Blank App
5. **Upload the application code** — via FTP, File Manager or SSH/SFTP into `/home/{user}/{app}/`
6. **Install dependencies** — Packages → Install from requirements.txt (client panel, or `POST …/pip/requirements` through the API)
7. **Set environment variables** — Environment Variables → DATABASE_URL, SECRET_KEY, etc.
8. **Run migrations** — manage.py → migrate
9. **Collect static files** — manage.py → collectstatic
10. **Create an admin user** — `manage.py createsuperuser` over SSH (it needs an interactive terminal)

The service was started when the app was created and fails to boot until the code is in place. With Auto Restart on, systemd retries it every 5 seconds, so it comes up once the code, packages and variables are ready and the application becomes reachable on the domain; otherwise start it from the `⋮` menu.

---

## Updating Application Code

To update an existing application:

1. Upload the new code (FTP / File Manager / SSH)
2. If there are new dependencies → Packages → Install from requirements.txt (client panel or API)
3. If there are new migrations → manage.py → migrate
4. If static files changed → manage.py → collectstatic
5. Restart

---

## Plan Limits

Hosting plans expose two limits for Python applications:

| Limit | Description | Default |
|-------|-------------|---------|
| Max Python Apps | Maximum number of applications per account | 0 (unlimited) |
| Python Workers / App | Most workers a single application may be given (0 = unlimited) | 4 |

Set these under Plans → Edit Plan → Package Limits; the account create and edit pages carry the same fields. The check reads the account's own values, so a plan change reaches an existing account through a package change or its edit page. Going over a limit is refused with `403 PYTHON_APP_LIMIT` or `403 PYTHON_WORKER_LIMIT`.

---

## File Layout

Creating a Python application produces the following layout on the server:

```
/home/{user}/
├── {app}/                       ← Application code
│   ├── manage.py
│   ├── gunicorn.conf.py         ← Generated automatically
│   ├── .env                     ← Environment variables (0600)
│   └── ...
├── venvs/{app}/                 ← Virtualenv
│   ├── bin/python
│   ├── bin/pip
│   └── bin/gunicorn             ← and bin/uvicorn for Uvicorn apps
└── logs/
    ├── {app}-error.log
    └── {app}-access.log

/etc/systemd/system/
└── whost-pyapp-{user}-{app}.service

/etc/nginx/sites-available/      ← /etc/nginx/conf.d/ on AlmaLinux, Rocky Linux, CentOS Stream
└── {domain}-pyapp-{app}.conf

/etc/logrotate.d/
└── whost-pyapp-{user}-{app}     ← log rotation, 14 compressed copies kept

/run/whost/{user}/
└── {app}.sock                   ← Unix socket
```

---

## API Reference

All endpoints live under the `/api/v1/` prefix.

### Admin Endpoints

| Method | Path | Description |
|--------|------|-------------|
| GET | `/accounts/{user}/python-apps` | List applications |
| POST | `/accounts/{user}/python-apps` | Create an application |
| GET | `/accounts/{user}/python-apps/{app}` | Detail |
| PUT | `/accounts/{user}/python-apps/{app}` | Update |
| DELETE | `/accounts/{user}/python-apps/{app}` | Delete |
| POST | `/accounts/{user}/python-apps/{app}/start` | Start |
| POST | `/accounts/{user}/python-apps/{app}/stop` | Stop |
| POST | `/accounts/{user}/python-apps/{app}/restart` | Restart |
| GET | `/accounts/{user}/python-apps/{app}/status` | Status |
| GET | `/accounts/{user}/python-apps/{app}/pip` | Package list |
| POST | `/accounts/{user}/python-apps/{app}/pip/install` | Install a package |
| POST | `/accounts/{user}/python-apps/{app}/pip/uninstall` | Uninstall a package |
| POST | `/accounts/{user}/python-apps/{app}/pip/requirements` | Install from requirements.txt |
| GET | `/accounts/{user}/python-apps/{app}/env` | Get environment variables |
| PUT | `/accounts/{user}/python-apps/{app}/env` | Set environment variables |
| GET | `/accounts/{user}/python-apps/{app}/logs` | Get logs |
| POST | `/accounts/{user}/python-apps/{app}/manage` | Run a manage.py command |
| POST | `/accounts/{user}/python-apps/analyze` | Smart Deploy: analyze an uploaded `archive` (multipart, up to 250 MB) or a `server_path` |
| POST | `/accounts/{user}/python-apps/migration/upload` | Smart Deploy: stage an SQL dump and/or media archive for the next deploy |
| POST | `/accounts/{user}/python-apps/deploy` | Smart Deploy: start the pipeline; returns a `task_id` |
| GET | `/accounts/{user}/python-apps/deploy/{task_id}` | Deploy task status and its events |
| GET | `/accounts/{user}/python-apps/deploy/{task_id}/events` | Live deploy log (Server-Sent Events) |
| GET | `/system/python/versions` | Installed Python versions |
| GET | `/system/python/apps` | All applications |

### Client Endpoints

Client endpoints live under the `/client/python-apps/` prefix and authenticate with the account's session cookie (or an API key bound to the account). The account name is not supplied — it is taken from that identity.

| Method | Path | Description |
|--------|------|-------------|
| GET | `/client/python-apps` | My applications |
| POST | `/client/python-apps` | Create an application |
| GET | `/client/python-apps/{app}` | Detail |
| PUT | `/client/python-apps/{app}` | Update |
| DELETE | `/client/python-apps/{app}` | Delete |
| POST | `/client/python-apps/{app}/start` | Start |
| POST | `/client/python-apps/{app}/stop` | Stop |
| POST | `/client/python-apps/{app}/restart` | Restart |
| GET | `/client/python-apps/{app}/status` | Status |
| GET | `/client/python-apps/{app}/pip` | Package list |
| POST | `/client/python-apps/{app}/pip/install` | Install a package |
| POST | `/client/python-apps/{app}/pip/uninstall` | Uninstall a package |
| POST | `/client/python-apps/{app}/pip/requirements` | Install from requirements.txt |
| GET | `/client/python-apps/{app}/env` | Get environment variables |
| PUT | `/client/python-apps/{app}/env` | Set environment variables |
| GET | `/client/python-apps/{app}/logs` | Get logs |
| POST | `/client/python-apps/{app}/manage` | Run a manage.py command |
| POST | `/client/python-apps/analyze` | Smart Deploy: analyze an uploaded `archive` (multipart, up to 250 MB) or a `server_path` |
| POST | `/client/python-apps/deploy` | Smart Deploy: start the pipeline; returns a `task_id` |
| GET | `/client/python-apps/deploy/{task_id}` | Deploy task status and its events |
| GET | `/client/python-apps/deploy/{task_id}/events` | Live deploy log (Server-Sent Events) |
| GET | `/client/python-apps/versions` | Installed Python versions |
