102 lines
7.1 KiB
Markdown
102 lines
7.1 KiB
Markdown
# Portainer deployment
|
|
|
|
Use `docker-compose.yml` as a Portainer Stack from this repository. It deploys the frontend, API, and PostgreSQL as one internal Docker network. Only the web container exposes a port; it proxies `/api` to the API container.
|
|
|
|
## Bunny Stream video hosting
|
|
|
|
To let instructors upload protected course videos, configure these API environment variables in the Portainer Stack. They are server secrets: never add a `VITE_` version or place them in the frontend container.
|
|
|
|
```env
|
|
BUNNY_STREAM_LIBRARY_ID=123456
|
|
BUNNY_STREAM_API_KEY=your-bunny-library-api-key
|
|
BUNNY_EMBED_TOKEN_KEY=your-bunny-embed-view-token-key
|
|
BUNNY_WEBHOOK_SECRET=your-bunny-read-only-api-key
|
|
BUNNY_EMBED_TOKEN_TTL_SECONDS=600
|
|
BUNNY_MAX_UPLOAD_MB=5120
|
|
```
|
|
|
|
In Bunny Stream, create one Video Library using the Volume tier. In that library's security settings, enable MediaCage Basic DRM and Embed View Token Authentication, then copy its token key into `BUNNY_EMBED_TOKEN_KEY`. Disable Direct Play and MP4 fallback if they are not needed, and add the Academy production hostname to Bunny's allowed referrers. This keeps playback inside Bunny's protected iframe and makes the Academy API issue a short-lived embed token only after a logged-in user requests a lesson.
|
|
|
|
The deployed Nginx proxy permits uploads up to **5 GB**. Keep `BUNNY_MAX_UPLOAD_MB` at or below `5120`; reduce it if you want a smaller application-level limit. Large uploads stream through the API rather than being held in memory.
|
|
|
|
After deploying the new images:
|
|
|
|
1. Sign in as an instructor and open **Gerenciar Cursos**.
|
|
2. Enter the lesson title first, then choose **Selecionar vídeo** in the Bunny Stream section.
|
|
3. Wait for the upload message, add the lesson, and save the course. Bunny encoding continues asynchronously.
|
|
4. The editor refreshes Bunny processing status automatically; it also has an **Atualizar** action for a pending lesson.
|
|
|
|
The API key and embed-token key never reach the browser. The instructor browser uploads to the authenticated Academy API, which sends the file to Bunny; learners receive only a signed iframe URL.
|
|
|
|
## Bunny Storage course banners
|
|
|
|
Course banners use Bunny Storage, not the Stream library. Create one Standard Storage Zone in São Paulo and link one Standard Pull Zone to it. The Pull Zone hostname is the public CDN host for uploaded banners. Add these server-only variables to Portainer:
|
|
|
|
```env
|
|
BUNNY_STORAGE_ZONE=compor-academy-storage
|
|
BUNNY_STORAGE_PASSWORD=the-storage-zone-password
|
|
BUNNY_STORAGE_ENDPOINT=https://the-storage-endpoint-shown-in-bunny
|
|
BUNNY_STORAGE_CDN_HOST=https://your-pull-zone.b-cdn.net
|
|
BUNNY_COVER_MAX_UPLOAD_MB=10
|
|
BUNNY_ASSET_MAX_UPLOAD_MB=100
|
|
```
|
|
|
|
The Storage Password is available in Bunny's **Storage Zone → FTP & API Access** section. Never expose it as a `VITE_` variable. In Academy, instructors can upload JPG, PNG, or WebP cover images and PDF, DOCX, XLSX, CSV, or ZIP support materials; the API validates the file bytes and determines the material type and size automatically. Covers are stored under `covers/` and course materials under `materials/`.
|
|
|
|
### Bunny processing webhooks
|
|
|
|
The editor can poll Bunny while a video encodes, but production should also configure Bunny's webhook so the Academy records the result even when no instructor page is open. In the library webhook settings, use:
|
|
|
|
```text
|
|
https://YOUR-DOMAIN/api/v1/webhooks/bunny
|
|
```
|
|
|
|
Set `BUNNY_WEBHOOK_SECRET` to Bunny's **Read-Only API key**. The Academy checks Bunny's HMAC signature against the unmodified request body and rejects unsigned requests. Do not use the normal library API key for this setting.
|
|
|
|
This is a Docker Swarm stack: Portainer pulls prebuilt API and web images from the Gitea Container Registry. It never builds Dockerfiles itself.
|
|
|
|
## Gitea Actions registry secrets
|
|
|
|
Create these repository-level Action secrets in Gitea before pushing to `main`:
|
|
|
|
- `REGISTRY_USERNAME`: the Gitea username that owns a package-write token.
|
|
- `REGISTRY_TOKEN`: a Gitea personal access token for that user with package read/write permission.
|
|
|
|
The built-in Actions job token can be disabled or lack registry scope on self-hosted Gitea instances, so the image publishing job intentionally uses these explicit secrets.
|
|
|
|
## Required Portainer environment variables
|
|
|
|
- `POSTGRES_PASSWORD`: a long, unique database password. Avoid characters that are not URL-safe because it is used in `DATABASE_URL`.
|
|
- `JWT_SECRET`: a unique random string of at least 32 characters.
|
|
- `FRONTEND_ORIGIN`: the exact public application URL, for example `https://hub.example.com`.
|
|
- `SUPERADMIN_EMAIL`: email address for the initial platform administrator.
|
|
- `SUPERADMIN_PASSWORD`: password for that administrator (at least 8 characters).
|
|
- `AUTH_RATE_LIMIT_MAX` and `AUTH_RATE_LIMIT_WINDOW_SECONDS` are optional login and public-auth throttling controls (defaults: 10 attempts per 900 seconds per source IP).
|
|
- `JWT_SESSION_TTL`, `INVITATION_TTL_HOURS`, `PASSWORD_RESET_TTL_HOURS`, `BUNNY_EMBED_TOKEN_TTL_SECONDS`, and `AUDIT_LOG_PAGE_SIZE` tune operating policy without changing code.
|
|
|
|
Optional variables:
|
|
|
|
- `POSTGRES_DB` (default `compor_hub`)
|
|
- `POSTGRES_USER` (default `compor`)
|
|
- `WEB_PORT` (default `8080`)
|
|
- `IMAGE_TAG` (default `latest`; set a specific release tag when available)
|
|
- `API_IMAGE` and `WEB_IMAGE` only if the Gitea registry namespace differs from the defaults.
|
|
|
|
## Before publishing
|
|
|
|
1. Push to `main` and wait for Gitea Actions to publish `gitea.blyzer.com.br/blyzer/compor-academy-api:latest` and `gitea.blyzer.com.br/blyzer/compor-academy-web:latest`.
|
|
2. Ensure the Portainer endpoint can pull from the Gitea Container Registry. If the images are private, add Gitea registry credentials to the endpoint/stack deployment configuration.
|
|
3. Deploy the stack with a temporary `WEB_PORT` and verify `/api/v1/health` through the public domain. A healthy response is `{"status":"ok","database":"connected"}`; Portainer also runs this check automatically for the API service.
|
|
4. Set `SUPERADMIN_EMAIL`, `SUPERADMIN_PASSWORD`, and optionally `SUPERADMIN_NAME`. The API creates or updates this superadmin automatically when it starts. Keep these values in Portainer only; changing the password and redeploying resets that account's password.
|
|
5. Place the web service behind HTTPS, normally through your existing reverse proxy (Traefik, Nginx Proxy Manager, or Cloudflare Tunnel), and set `FRONTEND_ORIGIN` to that HTTPS address.
|
|
6. Back up the `compor_postgres_data` volume before updates.
|
|
|
|
## Reliability checklist
|
|
|
|
- Keep the API at one replica until PostgreSQL capacity and upload traffic justify scaling. Auth throttling is database-backed, so it will remain consistent if you later add replicas.
|
|
- Point an external monitor at `https://YOUR-DOMAIN/api/v1/ready`; alert when it returns anything other than HTTP 200.
|
|
- Test a PostgreSQL backup restoration into a separate temporary database at least once per quarter. A backup is only proven when it restores.
|
|
- Create a separate Portainer stack and database for staging. Use a different `FRONTEND_ORIGIN`, `JWT_SECRET`, Bunny library, and `WEB_PORT`; never point staging at production PostgreSQL or video credentials.
|
|
|
|
Do not expose port 5432 or port 3001 publicly.
|