diff --git a/knowledge-base/assets.mdx b/knowledge-base/assets.mdx index 2b39e3a..477b714 100644 --- a/knowledge-base/assets.mdx +++ b/knowledge-base/assets.mdx @@ -54,4 +54,4 @@ There's no public API for the asset library yet. The closest equivalents are pos ## Self-hosted - Stock-photo and GIF tabs require valid `UNSPLASH_ACCESS_KEY` and `GIPHY_API_KEY` in your `.env`. Without them, those tabs stay disabled — the Library tab works regardless. -- Files are stored on the disk configured by `FILESYSTEM_DISK` (defaults to `r2` in shipped config). For production, point this at S3, R2, or any S3-compatible service. See the [Configuration guide](/self-hosting/configuration). +- Files are stored on the disk configured by `FILESYSTEM_DISK` (defaults to `public`). For production with multiple instances or heavy media, point this at S3, R2, Spaces, or any S3-compatible service. See the [Configuration guide](/self-hosting/configuration). diff --git a/knowledge-base/media.mdx b/knowledge-base/media.mdx index 27971c6..375b6c4 100644 --- a/knowledge-base/media.mdx +++ b/knowledge-base/media.mdx @@ -126,9 +126,10 @@ Media files are stored on the disk configured by `FILESYSTEM_DISK`. Out of the b | Driver | Use case | |--------|----------| -| `public` | Self-hosted setups serving files directly from `${APP_URL}/storage` — requires `php artisan storage:link` | +| `public` | Default — serve files from `${APP_URL}/storage` (requires `php artisan storage:link`) | | `s3` | AWS S3 | -| `r2` | Cloudflare R2 (S3-compatible — the default in shipped config) | +| `r2` | Cloudflare R2 (S3-compatible) | +| `spaces` | DigitalOcean Spaces | Any S3-compatible service (MinIO, DigitalOcean Spaces, Backblaze B2) works under the `s3` driver with the right endpoint. diff --git a/platforms/telegram.mdx b/platforms/telegram.mdx index 360f0a2..8fb08a7 100644 --- a/platforms/telegram.mdx +++ b/platforms/telegram.mdx @@ -54,5 +54,5 @@ php artisan telegram:set-webhook ``` - The webhook URL is `{APP_URL}/telegram/webhook`, so `APP_URL` must be a **public HTTPS URL** Telegram can reach. Re-run `telegram:set-webhook` whenever it changes. See the [Self-Hosting configuration guide](/self-hosting/configuration). + The webhook URL is `{WEBHOOK_URL or APP_URL}/telegram/webhook`. That base URL must be a **public HTTPS** address Telegram can reach. If `APP_URL` is private (or you use a tunnel in development), set `WEBHOOK_URL` instead — see [Configuration](/self-hosting/configuration#basic-configuration). Re-run `telegram:set-webhook` whenever it changes. diff --git a/self-hosting/configuration.mdx b/self-hosting/configuration.mdx index a9a1308..82b350b 100644 --- a/self-hosting/configuration.mdx +++ b/self-hosting/configuration.mdx @@ -15,6 +15,7 @@ APP_NAME="TryPost" APP_ENV=local APP_DEBUG=true APP_URL=http://localhost +WEBHOOK_URL= ``` | Variable | Description | @@ -23,6 +24,7 @@ APP_URL=http://localhost | `APP_ENV` | Environment: `local`, `staging`, `production` | | `APP_DEBUG` | Enable debug mode (set to `false` in production) | | `APP_URL` | Your application URL | +| `WEBHOOK_URL` | Optional public base URL for inbound webhooks (e.g. Telegram) when `APP_URL` is not reachable from the internet. Defaults to `APP_URL`. | ## Self-Hosted Mode @@ -32,7 +34,7 @@ SELF_HOSTED=true `SELF_HOSTED=true` is the default. It bypasses everything billing-related so you never have to touch Stripe or Cashier, and it locks down public sign-ups: -- **Public registration is disabled** — `/register` is closed so random users can't create accounts on your instance. Bootstrap the first admin via the `UserSeeder` (see [Create the admin user](/self-hosting/installation#3-create-the-admin-user)); after that, every account comes from a workspace invite (Settings → Members). +- **Public registration is disabled** — `/register` is closed so random users can't create accounts on your instance. Bootstrap the first admin via the `UserSeeder` (see [Seed default data and create the admin user](/self-hosting/installation#6-seed-passport-client-and-admin-user)); after that, every account comes from a workspace invite (Settings → Members). - No Stripe, Cashier, or `CASHIER_TRIAL_DAYS` configuration required - The `LoadWorkspaceFromToken` middleware skips the `402 Payment Required` gate - Subscription gating is not enforced, so workspaces, social accounts, members, and AI credits are unlimited per account @@ -76,29 +78,63 @@ REDIS_PASSWORD=null REDIS_PORT=6379 ``` +## Runtime drivers + +For a working production-like stack, point the Laravel drivers at Redis / Reverb / the database: + +```env +QUEUE_CONNECTION=redis +CACHE_STORE=redis +SESSION_DRIVER=database +BROADCAST_CONNECTION=reverb +``` + +| Variable | Recommended | Why | +|----------|-------------|-----| +| `QUEUE_CONNECTION` | `redis` | Horizon processes Redis queues (publishing, notifications, automations) | +| `CACHE_STORE` | `redis` | Cache + `onOneServer()` schedule locks across instances | +| `SESSION_DRIVER` | `database` | Durable sessions behind multiple app workers | +| `BROADCAST_CONNECTION` | `reverb` | Real-time dashboard updates | + +Without Redis queues, Horizon has nothing useful to process. Without Redis cache on multi-instance deploys, scheduled jobs can double-fire. + +## Passport (API & MCP tokens) + +Self-hosted API keys and MCP Bearer auth need both: + +```bash +php artisan passport:keys # once — RSA files in storage/ +php artisan db:seed # once — Personal Access Client in the DB +``` + +Alternatively, set `PASSPORT_PRIVATE_KEY` and `PASSPORT_PUBLIC_KEY` in `.env` (PEM contents) instead of key files — useful for secret managers. Do not regenerate keys after users have issued tokens. + +Full install order: [Installation](/self-hosting/installation). + + ## Media size limits ```env MEDIA_IMAGE_MAX_SIZE_MB=10 MEDIA_VIDEO_MAX_SIZE_MB=1024 -MCP_UPLOAD_MAX_SIZE_MB=50 -MCP_UPLOAD_URL_TTL_MINUTES=15 +MEDIA_DOCUMENT_MAX_SIZE_MB=100 +MEDIA_SIGNED_UPLOAD_URL_TTL_MINUTES=15 ``` | Variable | Default | Description | |----------|---------|-------------| | `MEDIA_IMAGE_MAX_SIZE_MB` | `10` | Max image upload size, in MB. Applied to direct uploads and URL fetches. | | `MEDIA_VIDEO_MAX_SIZE_MB` | `1024` | Max video upload size, in MB. Applied to direct uploads and URL fetches. | -| `MCP_UPLOAD_MAX_SIZE_MB` | `50` | Hard cap (in MB) for files uploaded through the MCP `request-media-upload-tool` signed-URL flow. Intentionally smaller than the per-type caps because uploads stream through the app server. Raise it if your operators / nginx are sized for larger payloads. | -| `MCP_UPLOAD_URL_TTL_MINUTES` | `15` | Lifetime of the signed upload URL returned by `request-media-upload-tool`. | +| `MEDIA_DOCUMENT_MAX_SIZE_MB` | `100` | Max document (PDF) upload size, in MB. | +| `MEDIA_SIGNED_UPLOAD_URL_TTL_MINUTES` | `15` | Lifetime of the signed upload URL returned by MCP / API media upload flows. (`MCP_UPLOAD_URL_TTL_MINUTES` remains as a legacy fallback.) | -If you raise the video limit, also bump PHP's `upload_max_filesize` and `post_max_size` and any reverse-proxy body-size cap to match. The MCP upload endpoint is additionally rate-limited at **10 requests / minute / IP** — this is a hard-coded defense; tune the surrounding nginx/Cloudflare limits if you need a different floor. +Signed uploads are rate-limited per workspace and per IP (`MEDIA_SIGNED_UPLOAD_PER_WORKSPACE_PER_MINUTE`, `MEDIA_SIGNED_UPLOAD_PER_IP_PER_MINUTE`). If you raise the video limit, also bump PHP's `upload_max_filesize` / `post_max_size` and any reverse-proxy body-size cap to match. ## File Storage TryPost supports local public storage and S3-compatible cloud storage. -The default disk is **Cloudflare R2** (`FILESYSTEM_DISK=r2`). For self-hosted installs without object storage configured, switch to `public`. +The application default is **`public`** (`FILESYSTEM_DISK=public`). Use object storage (S3 / R2 / Spaces) when you outgrow local disk or run multiple app instances. ### Public Disk @@ -108,12 +144,14 @@ Stores files in `storage/app/public` and serves them directly from `${APP_URL}/s FILESYSTEM_DISK=public ``` -After switching, create the symlink from `public/storage` to `storage/app/public`: +Create the symlink from `public/storage` to `storage/app/public` (once per install): ```bash php artisan storage:link ``` +Only needed for the `public` disk. Object storage (`s3`, `r2`, `spaces`) does not use this symlink. + See Laravel's [public disk documentation](https://laravel.com/docs/13.x/filesystem#the-public-disk) for more details. ### AWS S3 @@ -443,6 +481,16 @@ HORIZON_ALLOWED_EMAILS=admin@example.com,dev@example.com If not set, Horizon dashboard will be inaccessible in non-local environments. +## Advanced + +```env +TRYPOST_ALLOW_PRIVATE_NETWORK=false +``` + +| Variable | Default | Description | +|----------|---------|-------------| +| `TRYPOST_ALLOW_PRIVATE_NETWORK` | `false` | When `true`, allows HTTP requests from TryPost (RSS, webhooks, URL media fetches, etc.) to private/internal network addresses. Keep `false` unless you intentionally need that for a private network install. | + ## Next Steps - [Connect your social accounts](/) diff --git a/self-hosting/docker.mdx b/self-hosting/docker.mdx index bd84cdd..597ebb9 100644 --- a/self-hosting/docker.mdx +++ b/self-hosting/docker.mdx @@ -1,19 +1,37 @@ --- title: Docker -description: Run TryPost in Docker with Laravel Sail +description: Run TryPost with Docker Compose icon: "docker" --- # Docker -TryPost ships with a working `compose.yaml` that uses [Laravel Sail](https://laravel.com/docs/sail) for a containerized dev environment. The compose file includes PostgreSQL 18, Redis, Mailpit (local SMTP UI), and Selenium for browser tests — no `sail:install` step needed. +TryPost ships two Compose files: + +| File | Purpose | +|------|---------| +| `compose.prod.yaml` | **Self-hosting in production** — pulls `ghcr.io/trypostit/trypost:latest`, Postgres, Redis, Horizon, Reverb, scheduler, optional Caddy TLS | +| `compose.yaml` | Local development — builds from source, bind-mounts the repo, Mailpit, Vite | ## Requirements - Docker Desktop (Mac, Windows) or Docker Engine (Linux) - Docker Compose v2 -## Quick start +--- + +## Production self-hosting + +No local PHP/Node install required. The published image includes the built frontend. + +### What you do vs what the container does + +| You (once) | Entrypoint / Supervisor (automatic) | +|------------|-------------------------------------| +| Set `APP_KEY`, `APP_URL`, DB passwords, `REVERB_APP_SECRET` | Wait for Postgres, run migrations | +| `docker compose … up -d` | `passport:keys` if missing, `storage:link` if missing | +| `db:seed` + `UserSeeder` | Cache config/routes/views/events | +| Optional: Caddy TLS profile | Start nginx, php-fpm, **Horizon**, **Reverb**, **schedule:work** | ### 1. Clone the repository @@ -22,181 +40,188 @@ git clone https://github.com/trypostit/trypost.git cd trypost ``` -### 2. Install Composer dependencies +You only need the repo for `compose.prod.yaml` (and optionally `Caddyfile`). The app image comes from GHCR. -If you have PHP 8.2+ on your host: +### 2. Generate an app key and edit the compose file ```bash -composer install +docker compose -f compose.prod.yaml run --rm app php artisan key:generate --show ``` -Otherwise use a throwaway container — no PHP install required: +Paste the value into `APP_KEY` in `compose.prod.yaml`. Also set: -```bash -docker run --rm \ - -u "$(id -u):$(id -g)" \ - -v "$(pwd):/var/www/html" \ - -w /var/www/html \ - laravelsail/php85-composer:latest \ - composer install --ignore-platform-reqs -``` +- `APP_URL` — your public URL (e.g. `https://post.yourdomain.com`) +- `DB_PASSWORD` / `POSTGRES_PASSWORD` (must match) +- `REVERB_APP_SECRET` + +Leave `REVERB_APP_KEY` as `trypost-reverb-key` unless you rebuild the image — it must match the key baked into the published frontend bundle. -### 3. Configure environment +Queues, cache, session, and broadcast are already set to Redis / Reverb / database in `compose.prod.yaml`. Configure **mail** (defaults to `log`) and optional social/AI credentials in the same file — see [Configuration](/self-hosting/configuration). + +### 3. Start the stack ```bash -cp .env.example .env +docker compose -f compose.prod.yaml up -d ``` -Update the database and Redis hosts so they point at the Sail containers: - -```env -APP_URL=http://localhost +On first boot the entrypoint: -DB_CONNECTION=pgsql -DB_HOST=pgsql -DB_PORT=5432 -DB_DATABASE=trypost -DB_USERNAME=sail -DB_PASSWORD=password +- waits for Postgres +- runs migrations +- creates `storage:link` if missing (unused if you later switch to S3 / R2 / Spaces) +- generates Passport keys if missing (`passport:keys`) +- caches config / routes / views / events +- starts nginx, php-fpm, Horizon, Reverb, and `schedule:work` -REDIS_HOST=redis -``` +It does **not** seed the database — next step. -### 4. Start the containers +### 4. Seed Passport client and admin user (one-time) ```bash -./vendor/bin/sail up -d +docker compose -f compose.prod.yaml exec app php artisan db:seed +docker compose -f compose.prod.yaml exec app php artisan db:seed --class=UserSeeder ``` -That brings up `laravel.test`, `pgsql`, `redis`, `mailpit`, and `selenium`. +| Seeder | Purpose | +|--------|---------| +| `DatabaseSeeder` | Passport Personal Access Client + plan rows | +| `UserSeeder` | Admin (`admin@trypost.it` / `password`) | -### 5. Generate the app key +Change the admin password on first login. Extra accounts come from workspace invites (Settings → Members). -```bash -./vendor/bin/sail artisan key:generate -``` + + Passport **keys** = entrypoint. Passport **Personal Access Client** = `db:seed`. API keys and MCP need both. + -Migrations run as part of the seed step below. +### 5. Optional HTTPS with Caddy -### 6. Seed default data and create the admin user - -Self-hosted installs default to `SELF_HOSTED=true`, which closes `/register`. On a fresh database, run this once: +1. Point DNS at the host +2. Set `APP_URL` to `https://your-domain` and `APP_DOMAIN` on the `caddy` service +3. Start with the proxy profile: ```bash -./vendor/bin/sail artisan migrate:fresh --seed && ./vendor/bin/sail artisan db:seed --class=UserSeeder +docker compose -f compose.prod.yaml --profile proxy up -d ``` -This rebuilds the schema, runs the default `DatabaseSeeder` (Passport personal access client + plan rows), and creates the admin user. +Without the profile, the app listens on `http://localhost:8000`. - - `migrate:fresh` **drops every table**. Only run it on a brand-new database. If you already have data, run `sail artisan db:seed && sail artisan db:seed --class=UserSeeder` instead. - - -Default credentials — **change the password on first login**: - -| Field | Value | -|---|---| -| Email | `admin@trypost.it` | -| Password | `password` | - -Additional accounts come from workspace invites (Settings → Members). - -### 7. Install npm packages and build the front-end +### 6. Verify ```bash -./vendor/bin/sail npm install -./vendor/bin/sail npm run build +docker compose -f compose.prod.yaml exec app php artisan horizon:status +docker compose -f compose.prod.yaml logs -f app ``` -For active development with hot reload, use `sail npm run dev` instead. +1. Open `APP_URL` and sign in as admin +2. Create an API key (Settings → API) — proves Passport keys + client exist +3. Schedule a post — Horizon must show as running -## Accessing TryPost +### Access | Service | URL | |---------|-----| -| TryPost dashboard | `http://localhost` | -| Mailpit (captured emails) | `http://localhost:8025` | -| Postgres | `localhost:5432` | -| Redis | `localhost:6379` | - -## Common Commands +| TryPost | `http://localhost:8000` (or your `APP_URL`) | +| Reverb | port `8080`, proxied under `/app/` and `/apps/` by in-container nginx | -```bash -# Start containers -./vendor/bin/sail up -d - -# Stop containers -./vendor/bin/sail down +### Reverb on a custom domain -# View logs -./vendor/bin/sail logs +The published image bakes `VITE_REVERB_*` at build time (`localhost:8080`). For HTTPS on your domain, rebuild: -# Run Artisan commands -./vendor/bin/sail artisan +```bash +docker build \ + --build-arg VITE_REVERB_HOST=post.yourdomain.com \ + --build-arg VITE_REVERB_PORT=443 \ + --build-arg VITE_REVERB_SCHEME=https \ + --build-arg VITE_REVERB_APP_KEY=trypost-reverb-key \ + -t trypost-app:custom \ + -f docker/Dockerfile \ + --target production \ + . +``` -# Run npm commands -./vendor/bin/sail npm +Point `compose.prod.yaml` at that image. Changing only runtime `REVERB_HOST` / `PORT` / `SCHEME` does not update the browser client. -# Access PostgreSQL -./vendor/bin/sail psql +### Common production commands -# Access Redis CLI -./vendor/bin/sail redis +```bash +docker compose -f compose.prod.yaml logs -f app +docker compose -f compose.prod.yaml exec app php artisan horizon:status +docker compose -f compose.prod.yaml exec app php artisan migrate --force +docker compose -f compose.prod.yaml down ``` -## Running the Queue Worker +--- -For background job processing (publishing, notifications, token refresh): +## Local development -```bash -./vendor/bin/sail artisan horizon -``` +Use `compose.yaml` when you want a bind-mounted source tree. -Or run it detached alongside the rest of the stack: +### 1. Clone and start ```bash -./vendor/bin/sail up -d -./vendor/bin/sail artisan horizon +git clone https://github.com/trypostit/trypost.git +cd trypost +docker compose up -d ``` -## Shell alias +On first boot the entrypoint can seed `.env` from `docker/.env.docker.example`, install Composer/npm deps if missing, generate `APP_KEY`, migrate, `storage:link`, and `passport:keys`. Horizon, Reverb, the scheduler, and Vite start via Supervisor. + +For local media URLs, set `FILESYSTEM_DISK=public` in `.env` (the Docker example ships with `local`) and ensure `storage:link` ran. -Add this alias to your shell profile (`~/.bashrc`, `~/.zshrc`) so you can drop the `./vendor/bin/` prefix: +### 2. Seed admin + Passport client ```bash -alias sail='./vendor/bin/sail' +docker compose exec app php artisan db:seed +docker compose exec app php artisan db:seed --class=UserSeeder ``` -Then: +Or wipe-and-rebuild on a brand-new database: ```bash -sail up -d -sail artisan migrate -sail npm run dev +docker compose exec app php artisan migrate:fresh --seed +docker compose exec app php artisan db:seed --class=UserSeeder ``` -## Production with Docker + + `migrate:fresh` **drops every table**. + -The shipped `compose.yaml` is tuned for development (debug mode, Mailpit, Selenium). For production: +| Field | Value | +|---|---| +| Email | `admin@trypost.it` | +| Password | `password` | -1. Use a slimmer compose file without Selenium/Mailpit and with persistent named volumes for `storage/app` and Postgres data -2. Set `APP_ENV=production`, `APP_DEBUG=false` -3. Terminate SSL at a reverse proxy (Nginx, Traefik, Caddy) -4. Configure long-running supervisors for Horizon, Reverb, and the Laravel scheduler — see [Production setup](/self-hosting/production) +### Access (dev) + +| Service | URL | +|---------|-----| +| TryPost | `http://localhost:8000` (or `APP_PORT`) | +| Mailpit | `http://localhost:8025` | +| Vite HMR | `http://localhost:5173` | +| Postgres | `localhost:5432` | +| Redis | `localhost:6379` | +| Reverb | `localhost:8080` | + +### Common development commands + +```bash +docker compose up -d +docker compose down +docker compose logs -f app +docker compose exec app php artisan +``` ## Troubleshooting ### Port conflicts -If port 80 is already in use, change `APP_PORT` in `.env`: - ```env APP_PORT=8080 +FORWARD_DB_PORT=5433 +FORWARD_MAILPIT_UI_PORT=8026 ``` -### Permission issues - -On Linux you may need: +### Permission issues (Linux) ```bash sudo chown -R $USER: . @@ -204,10 +229,29 @@ sudo chown -R $USER: . ### Database connection refused -Make sure the database container is healthy: +```bash +docker compose ps +# or +docker compose -f compose.prod.yaml ps +``` + +### API key creation fails with "Personal access client not found" + +Seed the Passport client: + +```bash +docker compose -f compose.prod.yaml exec app php artisan db:seed +``` + +### Horizon not processing jobs ```bash -./vendor/bin/sail ps +docker compose -f compose.prod.yaml exec app php artisan horizon:status +docker compose -f compose.prod.yaml logs app | grep -i horizon ``` -Give Postgres a few seconds to initialize on first boot. +## Next steps + +- [Configuration](/self-hosting/configuration) — mail, storage, social OAuth, AI +- [Production setup](/self-hosting/production) — bare-metal Nginx / Supervisor if you are not using `compose.prod.yaml` +- [Installation](/self-hosting/installation) — clone-and-install without Docker diff --git a/self-hosting/installation.mdx b/self-hosting/installation.mdx index 5d83b89..6118394 100644 --- a/self-hosting/installation.mdx +++ b/self-hosting/installation.mdx @@ -6,12 +6,12 @@ icon: "download" # Installation -This guide walks through installing TryPost on a server (bare-metal or VM). For Docker-first setups, see [Docker](/self-hosting/docker). +Step-by-step install on a bare-metal server or VM. Prefer containers? Use [Docker production](/self-hosting/docker#production-recommended-for-self-hosting) instead. ## Requirements -- PHP 8.2 or higher -- Node.js 20 or higher +- PHP 8.2+ (with the extensions listed in [Requirements](/self-hosting/requirements)) +- Node.js 20+ - PostgreSQL 14+ or MySQL 8+ - Redis 6+ - Composer 2.x @@ -25,76 +25,174 @@ git clone https://github.com/trypostit/trypost.git cd trypost ``` -### 2. First-time setup (one command) +### 2. Install PHP and Node dependencies ```bash -composer setup +composer install +cp .env.example .env +php artisan key:generate +npm install ``` -This runs `composer install`, copies `.env.example` to `.env`, generates `APP_KEY`, runs `php artisan migrate --force`, installs npm packages, and runs `npm run build`. After it finishes, edit `.env` to point at your real database, Redis, and any social platform credentials — see [Configuration](/self-hosting/configuration). +`php artisan key:generate` writes `APP_KEY` into `.env`. Without it the app will not boot. + +### 3. Configure the environment + +Edit `.env` **before** migrating. Minimum for a working self-hosted instance: -If you prefer doing each step yourself: +```env +APP_URL=https://your-domain.com +APP_ENV=production +APP_DEBUG=false + +SELF_HOSTED=true + +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=trypost +DB_USERNAME=postgres +DB_PASSWORD=your_password + +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 + +QUEUE_CONNECTION=redis +CACHE_STORE=redis +SESSION_DRIVER=database +BROADCAST_CONNECTION=reverb + +FILESYSTEM_DISK=public +``` + +| Block | Why | +|-------|-----| +| `APP_KEY` | Already set by `key:generate` — encrypts sessions, cookies, encrypted data | +| Database | Where posts, users, and Passport clients live | +| Redis + queue/cache drivers | Horizon publishes posts and runs automations from Redis queues | +| `BROADCAST_CONNECTION=reverb` | Live dashboard updates | +| `FILESYSTEM_DISK=public` | Local media on disk (use `s3` / `r2` / `spaces` for object storage) | +| `SELF_HOSTED=true` | Disables Stripe billing and public `/register` | + +Also configure **mail** (invites / password reset) and optional social OAuth / AI keys — see [Configuration](/self-hosting/configuration). + + + Prefer a one-liner for deps? `composer setup` runs install + `key:generate` + migrate + npm build, but it migrates **before** you edit `.env`. Safer path for a real server: follow the steps above (configure `.env`, then migrate yourself). + + +### 4. Run migrations ```bash -composer install -cp .env.example .env -php artisan key:generate -# edit .env now -php artisan migrate -npm install -npm run build +php artisan migrate --force ``` -### 3. Seed default data and create the admin user +### 5. Passport keys (and local storage symlink) -Self-hosted installs default to `SELF_HOSTED=true`, which closes `/register` to the public — your instance only exists for you and the people you invite. +Passport needs an RSA key pair on disk to **sign and verify** API / MCP tokens: -On a fresh database, run this once: +```bash +php artisan passport:keys +``` + +Run once. Do **not** regenerate later unless you intend to invalidate every existing API token. + +If you keep `FILESYSTEM_DISK=public`, also create the public media symlink: ```bash -php artisan migrate:fresh --seed && php artisan db:seed --class=UserSeeder +php artisan storage:link ``` -This rebuilds the schema, runs the default `DatabaseSeeder` (which provisions the **Laravel Passport Personal Access Client** required to issue API tokens, plus the plan rows), and then creates the admin user. +Skip `storage:link` when using object storage (`s3`, `r2`, `spaces`) — those disks serve from the bucket URL. - - `migrate:fresh` **drops every table**. Only run it on a brand-new database during initial install. If you already have data, run `php artisan db:seed && php artisan db:seed --class=UserSeeder` instead — both seeders are idempotent. - +### 6. Seed Passport client and admin user + +Self-hosted installs close `/register`. Bootstrap the database once: + +```bash +php artisan db:seed +php artisan db:seed --class=UserSeeder +``` + +| Seeder | What it creates | +|--------|-----------------| +| `DatabaseSeeder` (`db:seed`) | **Passport Personal Access Client** (required to issue API tokens) + plan rows | +| `UserSeeder` | Admin account | + + + Passport is two independent pieces: + + 1. **Keys** (`passport:keys`) — files on disk + 2. **Personal Access Client** (`db:seed`) — row in `oauth_clients` -The admin account ships with fixed credentials — **change the password immediately on first login**: + API keys in Settings and MCP Bearer auth need **both**. + + +Admin credentials — **change the password immediately on first login**: | Field | Value | |---|---| | Email | `admin@trypost.it` | | Password | `password` | -After this, every other account on the instance comes from a workspace invite (Settings → Members) — invitees get a link that lets them sign up even though open registration stays off. +Further accounts come from workspace invites (Settings → Members). + + + Only on a brand-new empty database you may use `php artisan migrate:fresh --seed && php artisan db:seed --class=UserSeeder`. `migrate:fresh` **drops every table**. + -### 4. Run all dev processes in one terminal +### 7. Build the frontend + +```bash +npm run build +``` + +### 8. Run the app + +**Local / smoke test** (one terminal): ```bash composer dev ``` -This concurrently starts: +This starts `php artisan serve`, a queue listener, log tail, and Vite. Open `http://localhost:8000` and sign in with the admin user. + +**Production** — do **not** use `composer dev`. You need long-running processes: -- `php artisan serve` — HTTP server on `http://localhost:8000` -- `php artisan queue:listen` — queue worker -- `php artisan pail` — live log tail -- `npm run dev` — Vite with hot reload +| Process | Command | Purpose | +|---------|---------|---------| +| Web (PHP-FPM + Nginx) | see [Production](/self-hosting/production#nginx-configuration) | Serve HTTP | +| **Horizon** | `php artisan horizon` | Queues — publish posts, notifications, automations | +| **Reverb** | `php artisan reverb:start` | WebSockets | +| **Scheduler** | cron → `php artisan schedule:run` | Due posts, token refresh, automations | -For Inertia SSR development, use `composer dev:ssr` instead. +Full Supervisor configs, Nginx (including Reverb `/app/` + `/apps/`), SSL, and caches: [Production setup](/self-hosting/production). -### 5. Production process supervisors +## End-to-end checklist -For production you don't use `composer dev`. Configure separate supervisor entries for the web server (PHP-FPM / Octane), Horizon (queues), Reverb (WebSockets), and the cron job for `php artisan schedule:run` — see [Production setup](/self-hosting/production). +| Step | Done when | +|------|-----------| +| `APP_KEY` set | `key:generate` ran | +| Database reachable | `.env` `DB_*` correct | +| Queues on Redis | `QUEUE_CONNECTION=redis` + Redis up | +| Migrations applied | `php artisan migrate` | +| Passport keys | `storage/oauth-private.key` exists | +| Passport client | `php artisan db:seed` ran | +| Admin user | `UserSeeder` ran; you can log in | +| Media (local) | `storage:link` if `FILESYSTEM_DISK=public` | +| Frontend built | `npm run build` | +| Horizon running | posts actually publish | +| Reverb + scheduler | live UI + cron jobs | +| Mail configured | invites / password reset work | -## Verify the install +## Verify -Visit `http://localhost:8000` (or your configured `APP_URL`) and sign in with the admin credentials you set in step 3. You'll land in the dashboard. +1. Sign in at `APP_URL` with the admin credentials +2. Create an API key under workspace Settings → API (fails with 500 if Passport keys or client are missing) +3. Upload a media file (404s if you use `public` disk without `storage:link`) +4. Schedule a test post and confirm Horizon processes it -## Next Steps +## Next steps -- [Configure your environment](/self-hosting/configuration) -- [Connect your first social account](/) -- [Production deployment](/self-hosting/production) +- [Configuration](/self-hosting/configuration) — mail, storage, social OAuth, AI +- [Production setup](/self-hosting/production) — Nginx, SSL, Horizon, Reverb, cron +- [Docker](/self-hosting/docker) — same stack via Compose diff --git a/self-hosting/overview.mdx b/self-hosting/overview.mdx index 558a58c..247cdaf 100644 --- a/self-hosting/overview.mdx +++ b/self-hosting/overview.mdx @@ -10,6 +10,23 @@ TryPost is open-source and can be self-hosted on your own server. This section c Don't want to manage infrastructure? Use [TryPost Cloud](https://app.trypost.it/register) — no setup required, free to get started. +## Pick a path + +| Path | Best when | Guide | +|------|-----------|-------| +| **Docker production** | You want the published image with Horizon, Reverb, and the scheduler already wired | [Docker → Production](/self-hosting/docker#production-recommended-for-self-hosting) | +| **Bare metal** | You install PHP/Node yourself on a VPS | [Installation](/self-hosting/installation) then [Production](/self-hosting/production) | +| **Docker development** | You contribute to the codebase locally | [Docker → Local development](/self-hosting/docker#local-development) | + +Every path needs the same building blocks: + +1. **App key** (`APP_KEY`) +2. **Database** configured and migrated +3. **Redis** + `QUEUE_CONNECTION=redis` (Horizon) +4. **Passport keys** + **Personal Access Client** (API / MCP tokens) +5. **Admin user** (`UserSeeder`) +6. Long-running **Horizon**, **Reverb**, and the **scheduler** (Docker prod starts these for you) + ## Deployment options @@ -17,7 +34,7 @@ TryPost is open-source and can be self-hosted on your own server. This section c Step-by-step guide to install TryPost on a server. - Deploy with Docker and Laravel Sail. + Deploy with Docker Compose — local development or production. @@ -28,7 +45,7 @@ TryPost is open-source and can be self-hosted on your own server. This section c Environment variables, database, mail, storage, and more. - Nginx, SSL, queue workers, cron jobs, and WebSockets. + Nginx, SSL, Horizon, cron, and WebSockets (bare metal). diff --git a/self-hosting/production.mdx b/self-hosting/production.mdx index d9db3f4..43e3c7c 100644 --- a/self-hosting/production.mdx +++ b/self-hosting/production.mdx @@ -6,11 +6,23 @@ icon: "rocket" # Production Setup -This guide covers deploying TryPost to a production environment. +This guide is for **bare metal / a VM** after you have completed [Installation](/self-hosting/installation) (app key, `.env`, migrations, Passport keys, seed, frontend build). -## Environment Configuration +Prefer containers? Use [`compose.prod.yaml`](/self-hosting/docker#production-recommended-for-self-hosting) — it already runs Horizon, Reverb, and the scheduler for you. -Set these values in your `.env` for production: +## Before you continue + +Confirm the install checklist is done: + +- [ ] `APP_KEY` set +- [ ] Database migrated +- [ ] `QUEUE_CONNECTION=redis` and Redis reachable +- [ ] `php artisan passport:keys` ran +- [ ] `php artisan db:seed` + `UserSeeder` ran +- [ ] `storage:link` ran **if** `FILESYSTEM_DISK=public` +- [ ] `npm run build` completed + +## Environment (production values) ```env APP_ENV=production @@ -18,24 +30,27 @@ APP_DEBUG=false APP_URL=https://your-domain.com SELF_HOSTED=true + +QUEUE_CONNECTION=redis +CACHE_STORE=redis +SESSION_DRIVER=database +BROADCAST_CONNECTION=reverb +FILESYSTEM_DISK=public ``` +See [Configuration](/self-hosting/configuration) for mail, object storage, and social OAuth. + ## Optimizations Run these commands after deployment: ```bash -# Cache configuration -php artisan config:cache +composer install --optimize-autoloader --no-dev -# Cache routes +php artisan config:cache php artisan route:cache - -# Cache views php artisan view:cache - -# Optimize autoloader -composer install --optimize-autoloader --no-dev +php artisan event:cache ``` ## Queue Worker @@ -46,8 +61,9 @@ TryPost uses queues for critical background processing: - **Sending notifications** (email and in-app) - **Verifying social account connections** - **Processing analytics events** +- **Running automations** -Without a running queue worker, scheduled posts will not be published. Use Supervisor to keep the queue worker running. +Without a running queue worker, scheduled posts will not be published. Use Supervisor to keep Horizon running. ### Install Supervisor @@ -112,6 +128,30 @@ server { add_header X-Content-Type-Options "nosniff"; charset utf-8; + client_max_body_size 1G; + + # Reverb WebSocket (Pusher protocol) + HTTP API + location /app/ { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 86400s; + proxy_send_timeout 86400s; + } + + location /apps/ { + proxy_pass http://127.0.0.1:8080; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } location / { try_files $uri $uri/ /index.php?$query_string; @@ -132,11 +172,13 @@ server { location ~ /\.(?!well-known).* { deny all; } - - client_max_body_size 2G; } ``` + + Both `/app/` (browser WebSocket) and `/apps/` (Pusher HTTP API) must be proxied to Reverb. Omitting `/apps/` causes flaky real-time updates. + + ## SSL Certificate Use Let's Encrypt for free SSL: @@ -148,14 +190,18 @@ sudo certbot --nginx -d your-domain.com ## Scheduled Tasks (Cron) -TryPost uses Laravel's task scheduler for several recurring jobs. The scheduler **must** run every minute or scheduled posts won't publish, expiring tokens won't refresh, and stuck posts won't recover. +TryPost uses Laravel's task scheduler for several recurring jobs. The scheduler **must** run every minute or scheduled posts won't publish, automations won't fire, expiring tokens won't refresh, and stuck posts won't recover. | Command | Schedule | Purpose | |---------|----------|---------| | `posts:process-scheduled` | every minute | Dispatches `PublishPost` jobs for posts that are due | +| `automation:fire-schedule` | every minute | Fires time-based automation triggers | +| `automation:process-delays` | every minute | Advances automation runs waiting on delay nodes | +| `automation:recover-stuck-runs` | every 5 minutes | Recovers automation runs stuck mid-flight | +| `automation:prune-dry-runs` | every 10 minutes | Prunes dry-run automation history | +| `social:refresh-expiring-tokens` | every 15 minutes | Refreshes OAuth tokens that are about to expire | +| `social:recover-stuck-posts` | every 30 minutes | Detects posts stuck in `publishing` and retries or fails them | | `social:check-connections` | daily | Verifies every connected social account still has a valid token | -| `social:refresh-expiring-tokens` | hourly | Refreshes OAuth tokens that are about to expire (proactive refresh) | -| `social:recover-stuck-posts` | every 30 minutes | Detects posts stuck in `publishing` and either retries or marks them failed | Add the Laravel scheduler to cron: @@ -170,7 +216,7 @@ Add this line: ``` - This is required. Without the cron job, scheduled posts will not be published automatically, and social tokens will expire silently. + This is required. Without the cron job, scheduled posts will not be published automatically, automations will stall, and social tokens will expire silently. ## WebSocket Server (Reverb) @@ -200,19 +246,7 @@ sudo supervisorctl update sudo supervisorctl start trypost-reverb ``` - - In production, proxy WebSocket connections through Nginx with SSL. Add this to your Nginx config: - - ```nginx - location /app { - proxy_pass http://127.0.0.1:8080; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection "upgrade"; - proxy_set_header Host $host; - } - ``` - +Proxy `/app/` and `/apps/` through Nginx as shown above. ## File Permissions @@ -225,18 +259,27 @@ sudo chmod -R 775 /var/www/trypost/storage sudo chmod -R 775 /var/www/trypost/bootstrap/cache ``` -## Checklist +Keep `storage/oauth-private.key` readable only by the app user (typically mode `600`). -Make sure all of these are running in production: +## Checklist -| Service | Command | Purpose | -|---------|---------|---------| -| **Horizon** | `php artisan horizon` | Processes queues (post publishing, notifications) | -| **Reverb** | `php artisan reverb:start` | Real-time WebSocket updates | -| **Scheduler** | `php artisan schedule:run` (cron) | Dispatches scheduled posts | +Make sure all of these are in place in production: + +| Item | How | Purpose | +|------|-----|---------| +| **App key** | `php artisan key:generate` | Encrypts sessions / cookies | +| **Passport keys** | `php artisan passport:keys` (once) | Sign/verify API & MCP tokens | +| **Passport client** | `php artisan db:seed` (once) | Issue personal access tokens | +| **Admin user** | `php artisan db:seed --class=UserSeeder` (once) | First login | +| **Storage link** | `php artisan storage:link` (once, if `FILESYSTEM_DISK=public`) | Local media URLs — skip for S3 / R2 / Spaces | +| **Redis** | `QUEUE_CONNECTION=redis` + Redis up | Queues, cache, schedule locks | +| **Mail** | SMTP or SendKit | Invites, password reset, alerts | +| **Horizon** | `php artisan horizon` (Supervisor) | Publishes posts, notifications, automations | +| **Reverb** | `php artisan reverb:start` (Supervisor) | Real-time WebSocket updates | +| **Scheduler** | cron → `schedule:run` every minute | Due posts, token refresh, automations | - If any of these services are not running, TryPost will not function correctly. Scheduled posts won't publish, notifications won't send, and the dashboard won't update in real time. + If Horizon, Reverb, or the scheduler are not running, TryPost will not function correctly. Scheduled posts won't publish, notifications won't send, and the dashboard won't update in real time. ## Monitoring @@ -254,4 +297,5 @@ Regularly backup: - Database - `.env` file +- Passport keys (`storage/oauth-*.key`) — losing them invalidates all API tokens - Uploaded media (if using local storage) diff --git a/self-hosting/requirements.mdx b/self-hosting/requirements.mdx index f0178fe..06b41f0 100644 --- a/self-hosting/requirements.mdx +++ b/self-hosting/requirements.mdx @@ -22,17 +22,19 @@ This guide covers the requirements for self-hosting TryPost. - **PHP 8.2+** with extensions: - BCMath, Ctype, JSON, Mbstring, OpenSSL, PDO, Tokenizer, XML + - **pcntl** (required by Horizon) + - **intl**, **zip**, **exif**, **sockets** - GD or Imagick (for image processing) - - Redis extension + - Redis extension (or use Predis via Composer — the Redis **service** is still required) - **Composer** 2.x -- **Node.js** 20+ and npm (Vite 7 + Vue 3 require Node 20 or newer) +- **Node.js** 20+ and npm (Vite 7 + Vue 3 require Node 20 or newer) — not needed if you only run the published Docker image - **PostgreSQL** 14+ or **MySQL** 8+ - **Redis** 6+ ### Optional -- **Nginx** or **Apache** (for production) -- **Supervisor** (for queue workers) +- **Nginx** or **Apache** (for production bare-metal) +- **Supervisor** (for Horizon, Reverb, and related workers) - **SSL certificate** (Let's Encrypt recommended) ## PHP Configuration @@ -40,12 +42,14 @@ This guide covers the requirements for self-hosting TryPost. Recommended `php.ini` settings: ```ini -upload_max_filesize = 2G -post_max_size = 2G +upload_max_filesize = 1G +post_max_size = 1G memory_limit = 512M max_execution_time = 600 ``` +Match these to your reverse-proxy body-size limit (`client_max_body_size` in Nginx, etc.). The published Docker image uses a 1G cap. + ## Supported Operating Systems TryPost can run on any OS that supports the required software: @@ -74,16 +78,18 @@ Other supported providers: ## Storage Considerations -Media files (images, videos) can be stored: +Media files (images, videos, documents) can be stored: -- **Locally** - On the server's disk -- **S3** - Amazon S3 -- **R2** - Cloudflare R2 -- **Any S3-compatible** - MinIO, DigitalOcean Spaces, etc. +- **Locally** — `FILESYSTEM_DISK=public` + `php artisan storage:link` +- **S3** — Amazon S3 +- **R2** — Cloudflare R2 +- **Spaces** — DigitalOcean Spaces +- **Any S3-compatible** — MinIO, etc. -For production with heavy media usage, cloud storage is recommended. +For production with heavy media usage, object storage is recommended. See [Configuration → File Storage](/self-hosting/configuration#file-storage). ## Next Steps -- [Production Setup](/self-hosting/production) +- [Installation](/self-hosting/installation) - [Docker Deployment](/self-hosting/docker) +- [Production Setup](/self-hosting/production)