# Truehost Reseller Panel

A ready-to-run billing and client area for hosting and domain resellers:
clients, invoices, hosting packages, domain sales through Truehost, M-PESA and
card payments, support tickets, and a white-label storefront.

One container runs **nginx**, **PHP-FPM 8.4** and the **scheduler** (cron).
It listens on port **8080** and connects to **your own MySQL 8 or
MariaDB 10.6+** database. No database is included in the image.

---

## Quick start

**1. Create the database** on your MySQL or MariaDB server:

```sql
CREATE DATABASE pnlcs CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
CREATE USER 'pnlcs'@'%' IDENTIFIED BY 'a-strong-password';
GRANT ALL PRIVILEGES ON pnlcs.* TO 'pnlcs'@'%';
```

**2. Generate an application key** (once, and keep it safe):

```bash
docker run --rm truehostcloud/reseller-panel php artisan key:generate --show
```

**3. Create an `.env` file** with your values (see [Environment variables](#environment-variables)):

```env
APP_KEY=base64:PASTE_THE_KEY_FROM_STEP_2
APP_NAME="Truehost Reseller"
APP_URL=https://billing.example.com

DB_HOST=mysql.example.com
DB_PORT=3306
DB_DATABASE=pnlcs
DB_USERNAME=pnlcs
DB_PASSWORD=a-strong-password

SESSION_DRIVER=file
CACHE_STORE=database
QUEUE_CONNECTION=sync

MAIL_FROM_ADDRESS=noreply@example.com
MAIL_FROM_NAME="Truehost Reseller"
```

**4. Run it:**

```bash
docker run -d --name reseller-panel \
  --env-file .env \
  -p 8080:8080 \
  -v reseller-uploads:/var/www/html/storage/app \
  --restart unless-stopped \
  truehostcloud/reseller-panel:latest
```

**5. Finish the setup** in your browser at `http://<server>:8080/install`.
The wizard creates your administrator account. After that, sign in at
`/admin/login`.

On start, the container waits for the database, runs the migrations and
starts all services. Follow it with `docker logs -f reseller-panel`.

---

## Environment variables

### Required

| Variable | Example | Description |
|---|---|---|
| `APP_KEY` | `base64:…` | Encryption key from step 2. **Never change it after installing**: it encrypts stored payment, registrar and server credentials. The container will not start without it. |
| `APP_URL` | `https://billing.example.com` | Public address of the panel. Used in emails, links and payment callbacks. Use `https://` in production. |
| `DB_HOST` | `mysql.example.com` | Database server. For a database on the Docker host, use `host.docker.internal` and add `--add-host=host.docker.internal:host-gateway`. |
| `DB_PORT` | `3306` | Database port. |
| `DB_DATABASE` | `pnlcs` | Database name (utf8mb4). |
| `DB_USERNAME` | `pnlcs` | Database user, with all privileges on that database. |
| `DB_PASSWORD` | | Database password. |

### Recommended

| Variable | Default | Description |
|---|---|---|
| `APP_NAME` | `Truehost Reseller` | Name of the panel, used where no company name is set yet. |
| `SESSION_DRIVER` | `file` | Keep `file`. Users are signed out when the container restarts. |
| `CACHE_STORE` | `database` | Keep `database`. |
| `QUEUE_CONNECTION` | `sync` | Keep `sync`. The image runs no separate queue worker. |
| `MAIL_FROM_ADDRESS` | | Default sender address. |
| `MAIL_FROM_NAME` | | Default sender name. |

### Optional

| Variable | Default | Description |
|---|---|---|
| `APP_DEBUG` | `false` | Keep `false` in production: `true` shows internal errors to visitors. |
| `APP_LOCALE` | `en` | Default language. |
| `SESSION_SECURE_COOKIE` | | Set to `true` when the panel is served over HTTPS. |
| `SESSION_LIFETIME` | `120` | Minutes before an idle session expires. |
| `LOG_LEVEL` | `warning` | `debug`, `info`, `warning`, `error`… |
| `LOG_DAILY_DAYS` | `14` | Days of logs kept inside the container. |
| `MYSQL_ATTR_SSL_CA` | | Path to a CA file, for TLS connections to the database. |
| `RUN_MIGRATIONS` | `1` | `0` skips database migrations on start. |
| `DB_WAIT_TIMEOUT` | `60` | Seconds to wait for the database before giving up. |

### Configured in the admin area, not here

SMTP mail servers, payment gateways (M-PESA, Paystack, Stripe, PayPal),
Truehost domain registrar credentials, hosting servers, branding, logo and
themes are all set in the admin area and stored in the database.

---

## Docker Compose

```yaml
services:
  panel:
    image: truehostcloud/reseller-panel:latest
    restart: unless-stopped
    env_file: .env
    ports:
      - "8080:8080"
    volumes:
      - uploads:/var/www/html/storage/app
    extra_hosts:
      - "host.docker.internal:host-gateway"

volumes:
  uploads:
```

```bash
docker compose up -d
```

## Kubernetes

Store the variables in a Secret and load them with `envFrom`:

```bash
kubectl create namespace reseller-panel
kubectl -n reseller-panel create secret generic reseller-panel --from-env-file=.env
```

```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: reseller-panel
  namespace: reseller-panel
spec:
  replicas: 1
  strategy:
    type: Recreate
  selector:
    matchLabels: { app: reseller-panel }
  template:
    metadata:
      labels: { app: reseller-panel }
    spec:
      containers:
        - name: panel
          image: truehostcloud/reseller-panel:latest   # pin a version tag in production
          ports:
            - containerPort: 8080
          envFrom:
            - secretRef:
                name: reseller-panel
          readinessProbe:
            httpGet: { path: /up, port: 8080 }
            initialDelaySeconds: 20
          livenessProbe:
            httpGet: { path: /up, port: 8080 }
            initialDelaySeconds: 60
          volumeMounts:
            - name: uploads
              mountPath: /var/www/html/storage/app
      volumes:
        - name: uploads
          persistentVolumeClaim:
            claimName: reseller-panel-uploads
```

Put a Service and an Ingress with TLS in front of port 8080. Proxy headers
are trusted, so links are built with `https://` behind TLS termination.

---

## Good to know

- **Run one replica.** The scheduler runs inside the container, and sessions
  and uploads are stored per container.
- **Keep uploads on a volume.** Mount `/var/www/html/storage/app` to keep
  ticket attachments, payment receipts and the uploaded logo and favicon
  across restarts and upgrades. Without it, those files are lost when the
  container is replaced. Everything else is in the database.
- **Back up the database** regularly (snapshots or `mysqldump`). Backups made
  inside the container are not kept.
- **Changing a variable:** update `.env` or the Secret, then restart the container.
- **Health check:** `GET /up` answers `200` when the panel is running.
- **Runs as root at the top level.** supervisord starts the services; PHP
  runs as `www-data` and the application code is read-only. Clusters that
  enforce `runAsNonRoot` need an exception for this image.

## Tags and upgrades

| Tag | Use |
|---|---|
| `latest` | The newest build. Good for trying the panel |
| `<version>-<build>`, e.g. `1.5.1-beta.1-a32c77` | A fixed build. **Use this in production**, so the image only changes when you choose |

To upgrade, pull the new tag and recreate the container. Migrations run on
start; your data stays in your database. The in-panel updater is turned off
in this image: upgrades come only as new images.

```bash
docker pull truehostcloud/reseller-panel:<new-tag>
docker rm -f reseller-panel
docker run -d --name reseller-panel --env-file .env -p 8080:8080 \
  -v reseller-uploads:/var/www/html/storage/app --restart unless-stopped \
  truehostcloud/reseller-panel:<new-tag>
```

## Troubleshooting

| Symptom | Fix |
|---|---|
| `ERROR: APP_KEY is not set` | Add `APP_KEY` (Quick start, step 2) |
| `database … is not reachable` | Check the `DB_*` values, and that the database accepts connections from the container's IP |
| Saved credentials stopped working after a redeploy | `APP_KEY` changed. Restore the original key |
| Logo or attachments vanished after a restart | Mount a volume at `/var/www/html/storage/app` |
| Links or payment callbacks use `http://` | Set `APP_URL` to the `https://` address |

Logs: `docker logs -f reseller-panel` (or `kubectl logs -f deploy/reseller-panel`).
