From 51187a90a719da3fd777863baeccc5c0a1e92f15 Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sat, 1 Aug 2026 13:12:18 -0400 Subject: [PATCH 1/5] docs(self-hosting): document Passport keys, storage link, and Docker prod Clarify one-time install steps (passport:keys vs Personal Access Client), rewrite Docker docs away from Sail onto compose.yaml / compose.prod.yaml, and fill production gaps (runtime drivers, Reverb /apps proxy, scheduler). Co-authored-by: Cursor --- self-hosting/configuration.mdx | 48 +++++-- self-hosting/docker.mdx | 236 ++++++++++++++++++--------------- self-hosting/installation.mdx | 62 +++++++-- self-hosting/overview.mdx | 2 +- self-hosting/production.mdx | 121 ++++++++++++----- self-hosting/requirements.mdx | 32 +++-- 6 files changed, 326 insertions(+), 175 deletions(-) diff --git a/self-hosting/configuration.mdx b/self-hosting/configuration.mdx index a9a1308..71eaa63 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#4-seed-default-data-and-create-the-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,49 @@ 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. + ## 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,7 +130,7 @@ 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 @@ -443,6 +465,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..c2fcb55 100644 --- a/self-hosting/docker.mdx +++ b/self-hosting/docker.mdx @@ -1,19 +1,28 @@ --- 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.yaml` | Local development — builds from `docker/Dockerfile`, bind-mounts the repo, Postgres 16, Redis, Mailpit | +| `compose.prod.yaml` | Production — pulls `ghcr.io/trypostit/trypost:latest`, runs Horizon / Reverb / scheduler inside the app container, optional Caddy TLS | ## Requirements - Docker Desktop (Mac, Windows) or Docker Engine (Linux) - Docker Compose v2 -## Quick start +--- + +## Production (recommended for self-hosting) + +The production stack pulls the published image — no local PHP/Node install required. ### 1. Clone the repository @@ -22,176 +31,174 @@ 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`) +- Database / Redis passwords marked `change me` +- `REVERB_APP_SECRET` -### 3. Configure environment +### 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: +On first boot the entrypoint automatically: -```env -APP_URL=http://localhost +- waits for Postgres +- runs migrations +- creates `storage:link` if missing +- generates Passport keys if missing (`passport:keys`) +- caches config / routes / views / events +- starts nginx, php-fpm, Horizon, Reverb, and `schedule:work` -DB_CONNECTION=pgsql -DB_HOST=pgsql -DB_PORT=5432 -DB_DATABASE=trypost -DB_USERNAME=sail -DB_PASSWORD=password +It does **not** seed the database. Do that once in the next step. -REDIS_HOST=redis -``` - -### 4. Start the containers +### 4. Seed Passport + 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`. - -### 5. Generate the app key +| Seeder | Purpose | +|--------|---------| +| `DatabaseSeeder` | Passport Personal Access Client + plan rows | +| `UserSeeder` | Admin account (`admin@trypost.it` / `password`) | -```bash -./vendor/bin/sail artisan key:generate -``` +Change the admin password immediately on first login. Additional accounts come from workspace invites (Settings → Members). -Migrations run as part of the seed step below. + + Passport **keys** are created by the entrypoint. The Personal Access **Client** comes from `db:seed`. API keys and MCP need both — see [Installation](/self-hosting/installation#3-passport-keys-and-public-storage). + -### 6. Seed default data and create the admin user +### 5. Optional HTTPS with Caddy -Self-hosted installs default to `SELF_HOSTED=true`, which closes `/register`. On a fresh database, run this once: +1. Point your domain's 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. - - - `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. - +Without the profile, the app is served on `http://localhost:8000`. -Default credentials — **change the password on first login**: +### Access -| Field | Value | -|---|---| -| Email | `admin@trypost.it` | -| Password | `password` | +| Service | URL | +|---------|-----| +| TryPost | `http://localhost:8000` (or your `APP_URL`) | +| Reverb (WebSocket) | port `8080` (proxied under `/app/` and `/apps/` by the in-container nginx) | -Additional accounts come from workspace invites (Settings → Members). +### Reverb host note -### 7. Install npm packages and build the front-end +The published image bakes `VITE_REVERB_*` at build time (defaults to `localhost:8080`). For a custom domain over HTTPS, rebuild the image with: ```bash -./vendor/bin/sail npm install -./vendor/bin/sail npm run build +docker build \ + --build-arg VITE_REVERB_HOST=post.yourdomain.com \ + --build-arg VITE_REVERB_PORT=443 \ + --build-arg VITE_REVERB_SCHEME=https \ + -t trypost-app:custom \ + -f docker/Dockerfile \ + --target production \ + . ``` -For active development with hot reload, use `sail npm run dev` instead. +…and point `compose.prod.yaml` at that image. Changing only runtime `REVERB_*` env vars does not update the browser client. -## Accessing TryPost - -| Service | URL | -|---------|-----| -| TryPost dashboard | `http://localhost` | -| Mailpit (captured emails) | `http://localhost:8025` | -| Postgres | `localhost:5432` | -| Redis | `localhost:6379` | - -## Common Commands +### Common production commands ```bash -# Start containers -./vendor/bin/sail up -d - -# Stop containers -./vendor/bin/sail down +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 +``` -# View logs -./vendor/bin/sail logs +--- -# Run Artisan commands -./vendor/bin/sail artisan +## Local development -# Run npm commands -./vendor/bin/sail npm +Use `compose.yaml` when you want a full stack with a bind-mounted source tree. -# Access PostgreSQL -./vendor/bin/sail psql +### 1. Clone and start -# Access Redis CLI -./vendor/bin/sail redis +```bash +git clone https://github.com/trypostit/trypost.git +cd trypost +docker compose up -d ``` -## Running the Queue Worker +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`. -For background job processing (publishing, notifications, token refresh): +### 2. Seed default data and create the admin user ```bash -./vendor/bin/sail artisan horizon +docker compose exec app php artisan db:seed +docker compose exec app php artisan db:seed --class=UserSeeder ``` -Or run it detached alongside the rest of the stack: +Or wipe-and-rebuild on a brand-new database: ```bash -./vendor/bin/sail up -d -./vendor/bin/sail artisan horizon +docker compose exec app php artisan migrate:fresh --seed +docker compose exec app php artisan db:seed --class=UserSeeder ``` -## Shell alias + + `migrate:fresh` **drops every table**. Only run it on a brand-new database. + -Add this alias to your shell profile (`~/.bashrc`, `~/.zshrc`) so you can drop the `./vendor/bin/` prefix: +Default credentials — **change the password on first login**: -```bash -alias sail='./vendor/bin/sail' -``` +| Field | Value | +|---|---| +| Email | `admin@trypost.it` | +| Password | `password` | -Then: +### Access (dev) -```bash -sail up -d -sail artisan migrate -sail npm run dev -``` +| Service | URL | +|---------|-----| +| TryPost dashboard | `http://localhost:8000` (or `APP_PORT`) | +| Mailpit (captured emails) | `http://localhost:8025` | +| Vite HMR | `http://localhost:5173` | +| Postgres | `localhost:5432` | +| Redis | `localhost:6379` | +| Reverb | `localhost:8080` | -## Production with Docker +### Common development commands -The shipped `compose.yaml` is tuned for development (debug mode, Mailpit, Selenium). For production: +```bash +docker compose up -d +docker compose down +docker compose logs -f app +docker compose exec app php artisan +docker compose exec app npm run dev +``` -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) +Horizon, Reverb, and the scheduler already run inside the app container via Supervisor — you usually don't start them separately in Docker. ## Troubleshooting ### Port conflicts -If port 80 is already in use, change `APP_PORT` in `.env`: +Change the host ports in `.env` (dev) or the `ports:` mappings in `compose.prod.yaml`: ```env APP_PORT=8080 +FORWARD_DB_PORT=5433 +FORWARD_MAILPIT_UI_PORT=8026 ``` ### Permission issues @@ -204,10 +211,23 @@ sudo chown -R $USER: . ### Database connection refused -Make sure the database container is healthy: +Wait until Postgres is healthy: ```bash -./vendor/bin/sail ps +docker compose ps +# or +docker compose -f compose.prod.yaml ps ``` -Give Postgres a few seconds to initialize on first boot. +### API key creation fails with "Personal access client not found" + +The container has Passport keys but you haven't seeded yet: + +```bash +docker compose -f compose.prod.yaml exec app php artisan db:seed +``` + +## Next steps + +- [Configuration](/self-hosting/configuration) — mail, storage, social OAuth, AI +- [Production setup](/self-hosting/production) — bare-metal Nginx / Supervisor if you're not using `compose.prod.yaml` diff --git a/self-hosting/installation.mdx b/self-hosting/installation.mdx index 5d83b89..6366e6f 100644 --- a/self-hosting/installation.mdx +++ b/self-hosting/installation.mdx @@ -16,6 +16,8 @@ This guide walks through installing TryPost on a server (bare-metal or VM). For - Redis 6+ - Composer 2.x +See [Requirements](/self-hosting/requirements) for the full list (PHP extensions, hardware, storage). + ## Install ### 1. Clone the repository @@ -25,13 +27,19 @@ git clone https://github.com/trypostit/trypost.git cd trypost ``` -### 2. First-time setup (one command) +### 2. First-time setup ```bash composer setup ``` -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). +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`. + + + `composer setup` does **not** seed the database, generate Passport keys, or create the public storage symlink. Those are one-time steps below. + + +After it finishes, edit `.env` to point at your real database, Redis, queue/cache drivers, and any social platform credentials — see [Configuration](/self-hosting/configuration). If you prefer doing each step yourself: @@ -45,22 +53,53 @@ npm install npm run build ``` -### 3. Seed default data and create the admin user +### 3. Passport keys and public storage + +Passport needs an RSA key pair on disk to **sign and verify** API / MCP tokens. This is separate from the Personal Access Client row created by the seeder in the next step. + +```bash +php artisan passport:keys +php artisan storage:link +``` + +| Command | What it creates | +|---------|-----------------| +| `passport:keys` | `storage/oauth-private.key` and `storage/oauth-public.key` | +| `storage:link` | Symlink `public/storage` → `storage/app/public` (required when `FILESYSTEM_DISK=public`) | + +Run both once on a fresh install. Do **not** regenerate Passport keys later unless you intend to invalidate every existing API token. + +### 4. Seed default data and create the admin user 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. On a fresh database, run this once: ```bash -php artisan migrate:fresh --seed && php artisan db:seed --class=UserSeeder +php artisan db:seed && php artisan db:seed --class=UserSeeder ``` -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. +This 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. + +If you prefer a wipe-and-rebuild on a brand-new empty database: + +```bash +php artisan migrate:fresh --seed && php artisan db:seed --class=UserSeeder +``` - `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. + `migrate:fresh` **drops every table**. Only run it on a brand-new database during initial install. If you already have data, stick to `php artisan db:seed && php artisan db:seed --class=UserSeeder` — both seeders are idempotent. + + Passport setup is two independent pieces: + + 1. **Keys** (`passport:keys`) — cryptography material on disk + 2. **Personal Access Client** (`PassportSeeder` via `db:seed`) — database row used by `createToken()` + + API keys in Settings and MCP Bearer auth need **both**. + + The admin account ships with fixed credentials — **change the password immediately on first login**: | Field | Value | @@ -70,7 +109,7 @@ The admin account ships with fixed credentials — **change the password immedia 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. -### 4. Run all dev processes in one terminal +### 5. Run all dev processes in one terminal ```bash composer dev @@ -85,13 +124,18 @@ This concurrently starts: For Inertia SSR development, use `composer dev:ssr` instead. -### 5. Production process supervisors +### 6. Production process supervisors 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). ## Verify the install -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. +Visit `http://localhost:8000` (or your configured `APP_URL`) and sign in with the admin credentials from step 4. You'll land in the dashboard. + +Optional smoke checks: + +- Create an API key under workspace Settings → API — if Passport keys or the Personal Access Client are missing, this fails with a 500 +- Upload a media file — if `storage:link` is missing and you use the `public` disk, URLs 404 ## Next Steps diff --git a/self-hosting/overview.mdx b/self-hosting/overview.mdx index 558a58c..a8bec9c 100644 --- a/self-hosting/overview.mdx +++ b/self-hosting/overview.mdx @@ -17,7 +17,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. diff --git a/self-hosting/production.mdx b/self-hosting/production.mdx index d9db3f4..2a03689 100644 --- a/self-hosting/production.mdx +++ b/self-hosting/production.mdx @@ -6,7 +6,7 @@ icon: "rocket" # Production Setup -This guide covers deploying TryPost to a production environment. +This guide covers deploying TryPost to a production environment on bare metal / a VM. If you prefer containers, use [`compose.prod.yaml`](/self-hosting/docker#production-recommended-for-self-hosting) instead — it already runs Horizon, Reverb, and the scheduler for you. ## Environment Configuration @@ -18,24 +18,45 @@ 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 the full variable list. + +## One-time bootstrap + +After migrations, run these once on a fresh server (see [Installation](/self-hosting/installation)): + +```bash +php artisan passport:keys +php artisan storage:link +php artisan db:seed +php artisan db:seed --class=UserSeeder ``` +| Step | Why | +|------|-----| +| `passport:keys` | RSA key pair to sign/verify API and MCP tokens | +| `storage:link` | Public media URLs when using the `public` disk | +| `db:seed` | Passport Personal Access Client + plan rows | +| `UserSeeder` | First admin account | + ## 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 +67,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 +134,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 +178,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 +196,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 +222,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 +252,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 +265,26 @@ 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) | +Make sure all of these are in place in production: + +| Item | How | Purpose | +|------|-----|---------| +| **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) | Public media URLs | +| **Redis** | `QUEUE_CONNECTION` / `CACHE_STORE` | Queues, cache, schedule locks | +| **Mail** | SMTP or SendKit | Invites, password reset, alerts | +| **Horizon** | `php artisan horizon` | Processes queues | | **Reverb** | `php artisan reverb:start` | Real-time WebSocket updates | -| **Scheduler** | `php artisan schedule:run` (cron) | Dispatches scheduled posts | +| **Scheduler** | `php artisan schedule:run` (cron) | Posts, automations, token refresh | - 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 +302,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) From ec2b9b5c87b78316a563a788285759ff434b4c0d Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sat, 1 Aug 2026 13:12:59 -0400 Subject: [PATCH 2/5] docs(self-hosting): storage:link only for the public disk Skip the symlink when media is on S3, R2, Spaces, or other object storage. Co-authored-by: Cursor --- self-hosting/configuration.mdx | 2 ++ self-hosting/docker.mdx | 4 ++-- self-hosting/installation.mdx | 18 ++++++++++-------- self-hosting/production.mdx | 13 ++++++++++--- 4 files changed, 24 insertions(+), 13 deletions(-) diff --git a/self-hosting/configuration.mdx b/self-hosting/configuration.mdx index 71eaa63..3d50c35 100644 --- a/self-hosting/configuration.mdx +++ b/self-hosting/configuration.mdx @@ -136,6 +136,8 @@ Create the symlink from `public/storage` to `storage/app/public` (once per insta 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 diff --git a/self-hosting/docker.mdx b/self-hosting/docker.mdx index c2fcb55..95a0772 100644 --- a/self-hosting/docker.mdx +++ b/self-hosting/docker.mdx @@ -55,7 +55,7 @@ On first boot the entrypoint automatically: - waits for Postgres - runs migrations -- creates `storage:link` if missing +- creates `storage:link` if missing (harmless no-op 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` @@ -77,7 +77,7 @@ docker compose -f compose.prod.yaml exec app php artisan db:seed --class=UserSee Change the admin password immediately on first login. Additional accounts come from workspace invites (Settings → Members). - Passport **keys** are created by the entrypoint. The Personal Access **Client** comes from `db:seed`. API keys and MCP need both — see [Installation](/self-hosting/installation#3-passport-keys-and-public-storage). + Passport **keys** are created by the entrypoint. The Personal Access **Client** comes from `db:seed`. API keys and MCP need both — see [Installation](/self-hosting/installation#3-passport-keys-and-local-storage-if-needed). ### 5. Optional HTTPS with Caddy diff --git a/self-hosting/installation.mdx b/self-hosting/installation.mdx index 6366e6f..2358895 100644 --- a/self-hosting/installation.mdx +++ b/self-hosting/installation.mdx @@ -53,21 +53,23 @@ npm install npm run build ``` -### 3. Passport keys and public storage +### 3. Passport keys (and local storage, if needed) Passport needs an RSA key pair on disk to **sign and verify** API / MCP tokens. This is separate from the Personal Access Client row created by the seeder in the next step. ```bash php artisan passport:keys -php artisan storage:link ``` -| Command | What it creates | -|---------|-----------------| -| `passport:keys` | `storage/oauth-private.key` and `storage/oauth-public.key` | -| `storage:link` | Symlink `public/storage` → `storage/app/public` (required when `FILESYSTEM_DISK=public`) | +Run once on a fresh install. Do **not** regenerate Passport keys later unless you intend to invalidate every existing API token. + +If you store media on the local **`public`** disk (`FILESYSTEM_DISK=public`, the app default), also create the public symlink once: + +```bash +php artisan storage:link +``` -Run both once on a fresh install. Do **not** regenerate Passport keys later unless you intend to invalidate every existing API token. +Skip `storage:link` when using object storage (`s3`, `r2`, `spaces`, etc.) — those disks serve files from the bucket URL, not from `public/storage`. ### 4. Seed default data and create the admin user @@ -135,7 +137,7 @@ Visit `http://localhost:8000` (or your configured `APP_URL`) and sign in with th Optional smoke checks: - Create an API key under workspace Settings → API — if Passport keys or the Personal Access Client are missing, this fails with a 500 -- Upload a media file — if `storage:link` is missing and you use the `public` disk, URLs 404 +- Upload a media file — if you use `FILESYSTEM_DISK=public` and skipped `storage:link`, media URLs 404 ## Next Steps diff --git a/self-hosting/production.mdx b/self-hosting/production.mdx index 2a03689..ac28f65 100644 --- a/self-hosting/production.mdx +++ b/self-hosting/production.mdx @@ -34,17 +34,24 @@ After migrations, run these once on a fresh server (see [Installation](/self-hos ```bash php artisan passport:keys -php artisan storage:link php artisan db:seed php artisan db:seed --class=UserSeeder ``` +Only if you use local media storage (`FILESYSTEM_DISK=public`): + +```bash +php artisan storage:link +``` + +Skip that command when media lives on S3, R2, Spaces, or another object store. + | Step | Why | |------|-----| | `passport:keys` | RSA key pair to sign/verify API and MCP tokens | -| `storage:link` | Public media URLs when using the `public` disk | | `db:seed` | Passport Personal Access Client + plan rows | | `UserSeeder` | First admin account | +| `storage:link` | Only for `FILESYSTEM_DISK=public` — not needed for S3 / R2 / Spaces | ## Optimizations @@ -276,7 +283,7 @@ Make sure all of these are in place in production: | **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) | Public media URLs | +| **Storage link** | `php artisan storage:link` (once, if `FILESYSTEM_DISK=public`) | Local media URLs — skip for S3 / R2 / Spaces | | **Redis** | `QUEUE_CONNECTION` / `CACHE_STORE` | Queues, cache, schedule locks | | **Mail** | SMTP or SendKit | Invites, password reset, alerts | | **Horizon** | `php artisan horizon` | Processes queues | From 92fad71369b44177d45ef30e50cf943701ef18eb Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sat, 1 Aug 2026 13:16:33 -0400 Subject: [PATCH 3/5] docs: fix review nits in self-hosting Docker and storage defaults Correct compose.prod secrets, avoid a second Vite process in Docker dev commands, and align the Assets KB with FILESYSTEM_DISK=public. Co-authored-by: Cursor --- knowledge-base/assets.mdx | 2 +- self-hosting/docker.mdx | 7 ++++--- self-hosting/installation.mdx | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) 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/self-hosting/docker.mdx b/self-hosting/docker.mdx index 95a0772..ceacdbf 100644 --- a/self-hosting/docker.mdx +++ b/self-hosting/docker.mdx @@ -42,9 +42,11 @@ docker compose -f compose.prod.yaml run --rm app php artisan key:generate --show Paste the value into `APP_KEY` in `compose.prod.yaml`. Also set: - `APP_URL` — your public URL (e.g. `https://post.yourdomain.com`) -- Database / Redis passwords marked `change me` +- `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. Start the stack ```bash @@ -184,10 +186,9 @@ docker compose up -d docker compose down docker compose logs -f app docker compose exec app php artisan -docker compose exec app npm run dev ``` -Horizon, Reverb, and the scheduler already run inside the app container via Supervisor — you usually don't start them separately in Docker. +Horizon, Reverb, the scheduler, and Vite already run inside the app container via Supervisor — you usually don't start them separately. ## Troubleshooting diff --git a/self-hosting/installation.mdx b/self-hosting/installation.mdx index 2358895..5620a1d 100644 --- a/self-hosting/installation.mdx +++ b/self-hosting/installation.mdx @@ -36,7 +36,7 @@ composer setup 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`. - `composer setup` does **not** seed the database, generate Passport keys, or create the public storage symlink. Those are one-time steps below. + `composer setup` does **not** seed the database or generate Passport keys. Those are one-time steps below. It also does not run `storage:link` (only needed if you keep `FILESYSTEM_DISK=public`). After it finishes, edit `.env` to point at your real database, Redis, queue/cache drivers, and any social platform credentials — see [Configuration](/self-hosting/configuration). From 092dc98a57f8c4892461fc3cf9e5c100ead65a1a Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sat, 1 Aug 2026 13:16:39 -0400 Subject: [PATCH 4/5] docs(media): stop calling R2 the default filesystem disk Co-authored-by: Cursor --- knowledge-base/media.mdx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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. From 656697c35944ba6a055dd13fb8c17944b0f3e73b Mon Sep 17 00:00:00 2001 From: Paulo Castellano Date: Sat, 1 Aug 2026 13:19:27 -0400 Subject: [PATCH 5/5] docs(self-hosting): make bare-metal and Docker paths end-to-end Reorder installation so .env is configured before migrate, add an explicit checklist covering APP_KEY, Passport, queues, Horizon, and seed; clarify Docker prod automation vs one-time seed; wire Passport and WEBHOOK_URL into configuration/Telegram. Co-authored-by: Cursor --- platforms/telegram.mdx | 2 +- self-hosting/configuration.mdx | 16 ++- self-hosting/docker.mdx | 95 ++++++++++------- self-hosting/installation.mdx | 180 +++++++++++++++++++++------------ self-hosting/overview.mdx | 19 +++- self-hosting/production.mdx | 54 ++++------ 6 files changed, 230 insertions(+), 136 deletions(-) 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 3d50c35..82b350b 100644 --- a/self-hosting/configuration.mdx +++ b/self-hosting/configuration.mdx @@ -34,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 [Seed default data and create the admin user](/self-hosting/installation#4-seed-default-data-and-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 @@ -98,6 +98,20 @@ BROADCAST_CONNECTION=reverb 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 diff --git a/self-hosting/docker.mdx b/self-hosting/docker.mdx index ceacdbf..597ebb9 100644 --- a/self-hosting/docker.mdx +++ b/self-hosting/docker.mdx @@ -10,8 +10,8 @@ TryPost ships two Compose files: | File | Purpose | |------|---------| -| `compose.yaml` | Local development — builds from `docker/Dockerfile`, bind-mounts the repo, Postgres 16, Redis, Mailpit | -| `compose.prod.yaml` | Production — pulls `ghcr.io/trypostit/trypost:latest`, runs Horizon / Reverb / scheduler inside the app container, optional Caddy TLS | +| `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 @@ -20,9 +20,18 @@ TryPost ships two Compose files: --- -## Production (recommended for self-hosting) +## Production self-hosting -The production stack pulls the published image — no local PHP/Node install required. +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 @@ -47,24 +56,26 @@ Paste the value into `APP_KEY` in `compose.prod.yaml`. Also set: Leave `REVERB_APP_KEY` as `trypost-reverb-key` unless you rebuild the image — it must match the key baked into the published frontend bundle. +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 docker compose -f compose.prod.yaml up -d ``` -On first boot the entrypoint automatically: +On first boot the entrypoint: - waits for Postgres - runs migrations -- creates `storage:link` if missing (harmless no-op if you later switch to S3 / R2 / Spaces) +- 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` -It does **not** seed the database. Do that once in the next step. +It does **not** seed the database — next step. -### 4. Seed Passport + admin user (one-time) +### 4. Seed Passport client and admin user (one-time) ```bash docker compose -f compose.prod.yaml exec app php artisan db:seed @@ -74,17 +85,17 @@ docker compose -f compose.prod.yaml exec app php artisan db:seed --class=UserSee | Seeder | Purpose | |--------|---------| | `DatabaseSeeder` | Passport Personal Access Client + plan rows | -| `UserSeeder` | Admin account (`admin@trypost.it` / `password`) | +| `UserSeeder` | Admin (`admin@trypost.it` / `password`) | -Change the admin password immediately on first login. Additional accounts come from workspace invites (Settings → Members). +Change the admin password on first login. Extra accounts come from workspace invites (Settings → Members). - Passport **keys** are created by the entrypoint. The Personal Access **Client** comes from `db:seed`. API keys and MCP need both — see [Installation](/self-hosting/installation#3-passport-keys-and-local-storage-if-needed). + Passport **keys** = entrypoint. Passport **Personal Access Client** = `db:seed`. API keys and MCP need both. ### 5. Optional HTTPS with Caddy -1. Point your domain's DNS at the host +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: @@ -92,31 +103,43 @@ Change the admin password immediately on first login. Additional accounts come f docker compose -f compose.prod.yaml --profile proxy up -d ``` -Without the profile, the app is served on `http://localhost:8000`. +Without the profile, the app listens on `http://localhost:8000`. + +### 6. Verify + +```bash +docker compose -f compose.prod.yaml exec app php artisan horizon:status +docker compose -f compose.prod.yaml logs -f app +``` + +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 ### Access | Service | URL | |---------|-----| | TryPost | `http://localhost:8000` (or your `APP_URL`) | -| Reverb (WebSocket) | port `8080` (proxied under `/app/` and `/apps/` by the in-container nginx) | +| Reverb | port `8080`, proxied under `/app/` and `/apps/` by in-container nginx | -### Reverb host note +### Reverb on a custom domain -The published image bakes `VITE_REVERB_*` at build time (defaults to `localhost:8080`). For a custom domain over HTTPS, rebuild the image with: +The published image bakes `VITE_REVERB_*` at build time (`localhost:8080`). For HTTPS on your domain, rebuild: ```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 \ . ``` -…and point `compose.prod.yaml` at that image. Changing only runtime `REVERB_*` env vars does not update the browser client. +Point `compose.prod.yaml` at that image. Changing only runtime `REVERB_HOST` / `PORT` / `SCHEME` does not update the browser client. ### Common production commands @@ -131,7 +154,7 @@ docker compose -f compose.prod.yaml down ## Local development -Use `compose.yaml` when you want a full stack with a bind-mounted source tree. +Use `compose.yaml` when you want a bind-mounted source tree. ### 1. Clone and start @@ -141,9 +164,11 @@ cd trypost docker compose up -d ``` -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`. +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. -### 2. Seed default data and create the admin user +### 2. Seed admin + Passport client ```bash docker compose exec app php artisan db:seed @@ -158,11 +183,9 @@ docker compose exec app php artisan db:seed --class=UserSeeder ``` - `migrate:fresh` **drops every table**. Only run it on a brand-new database. + `migrate:fresh` **drops every table**. -Default credentials — **change the password on first login**: - | Field | Value | |---|---| | Email | `admin@trypost.it` | @@ -172,8 +195,8 @@ Default credentials — **change the password on first login**: | Service | URL | |---------|-----| -| TryPost dashboard | `http://localhost:8000` (or `APP_PORT`) | -| Mailpit (captured emails) | `http://localhost:8025` | +| TryPost | `http://localhost:8000` (or `APP_PORT`) | +| Mailpit | `http://localhost:8025` | | Vite HMR | `http://localhost:5173` | | Postgres | `localhost:5432` | | Redis | `localhost:6379` | @@ -188,23 +211,17 @@ docker compose logs -f app docker compose exec app php artisan ``` -Horizon, Reverb, the scheduler, and Vite already run inside the app container via Supervisor — you usually don't start them separately. - ## Troubleshooting ### Port conflicts -Change the host ports in `.env` (dev) or the `ports:` mappings in `compose.prod.yaml`: - ```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: . @@ -212,8 +229,6 @@ sudo chown -R $USER: . ### Database connection refused -Wait until Postgres is healthy: - ```bash docker compose ps # or @@ -222,13 +237,21 @@ docker compose -f compose.prod.yaml ps ### API key creation fails with "Personal access client not found" -The container has Passport keys but you haven't seeded yet: +Seed the Passport client: ```bash docker compose -f compose.prod.yaml exec app php artisan db:seed ``` +### Horizon not processing jobs + +```bash +docker compose -f compose.prod.yaml exec app php artisan horizon:status +docker compose -f compose.prod.yaml logs app | grep -i horizon +``` + ## Next steps - [Configuration](/self-hosting/configuration) — mail, storage, social OAuth, AI -- [Production setup](/self-hosting/production) — bare-metal Nginx / Supervisor if you're not using `compose.prod.yaml` +- [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 5620a1d..6118394 100644 --- a/self-hosting/installation.mdx +++ b/self-hosting/installation.mdx @@ -6,18 +6,16 @@ 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 -See [Requirements](/self-hosting/requirements) for the full list (PHP extensions, hardware, storage). - ## Install ### 1. Clone the repository @@ -27,120 +25,174 @@ git clone https://github.com/trypostit/trypost.git cd trypost ``` -### 2. First-time setup +### 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`. +`php artisan key:generate` writes `APP_KEY` into `.env`. Without it the app will not boot. - - `composer setup` does **not** seed the database or generate Passport keys. Those are one-time steps below. It also does not run `storage:link` (only needed if you keep `FILESYSTEM_DISK=public`). - +### 3. Configure the environment -After it finishes, edit `.env` to point at your real database, Redis, queue/cache drivers, and any social platform credentials — see [Configuration](/self-hosting/configuration). +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 -```bash -composer install -cp .env.example .env -php artisan key:generate -# edit .env now -php artisan migrate -npm install -npm run build -``` +SELF_HOSTED=true -### 3. Passport keys (and local storage, if needed) +DB_CONNECTION=pgsql +DB_HOST=127.0.0.1 +DB_PORT=5432 +DB_DATABASE=trypost +DB_USERNAME=postgres +DB_PASSWORD=your_password -Passport needs an RSA key pair on disk to **sign and verify** API / MCP tokens. This is separate from the Personal Access Client row created by the seeder in the next step. +REDIS_HOST=127.0.0.1 +REDIS_PORT=6379 -```bash -php artisan passport:keys +QUEUE_CONNECTION=redis +CACHE_STORE=redis +SESSION_DRIVER=database +BROADCAST_CONNECTION=reverb + +FILESYSTEM_DISK=public ``` -Run once on a fresh install. Do **not** regenerate Passport keys later unless you intend to invalidate every existing API token. +| 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). + -If you store media on the local **`public`** disk (`FILESYSTEM_DISK=public`, the app default), also create the public symlink once: +### 4. Run migrations ```bash -php artisan storage:link +php artisan migrate --force ``` -Skip `storage:link` when using object storage (`s3`, `r2`, `spaces`, etc.) — those disks serve files from the bucket URL, not from `public/storage`. +### 5. Passport keys (and local storage symlink) + +Passport needs an RSA key pair on disk to **sign and verify** API / MCP tokens: -### 4. Seed default data and create the admin user +```bash +php artisan passport:keys +``` -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. +Run once. Do **not** regenerate later unless you intend to invalidate every existing API token. -On a fresh database, run this once: +If you keep `FILESYSTEM_DISK=public`, also create the public media symlink: ```bash -php artisan db:seed && php artisan db:seed --class=UserSeeder +php artisan storage:link ``` -This 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. + +### 6. Seed Passport client and admin user -If you prefer a wipe-and-rebuild on a brand-new empty database: +Self-hosted installs close `/register`. Bootstrap the database once: ```bash -php artisan migrate:fresh --seed && php artisan db:seed --class=UserSeeder +php artisan db:seed +php artisan db:seed --class=UserSeeder ``` - - `migrate:fresh` **drops every table**. Only run it on a brand-new database during initial install. If you already have data, stick to `php artisan db:seed && php artisan db:seed --class=UserSeeder` — both seeders are idempotent. - +| Seeder | What it creates | +|--------|-----------------| +| `DatabaseSeeder` (`db:seed`) | **Passport Personal Access Client** (required to issue API tokens) + plan rows | +| `UserSeeder` | Admin account | - Passport setup is two independent pieces: + Passport is two independent pieces: - 1. **Keys** (`passport:keys`) — cryptography material on disk - 2. **Personal Access Client** (`PassportSeeder` via `db:seed`) — database row used by `createToken()` + 1. **Keys** (`passport:keys`) — files on disk + 2. **Personal Access Client** (`db:seed`) — row in `oauth_clients` API keys in Settings and MCP Bearer auth need **both**. -The admin account ships with fixed credentials — **change the password immediately on first login**: +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). -### 5. Run all dev processes in one terminal + + 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**. + + +### 7. Build the frontend ```bash -composer dev +npm run build ``` -This concurrently starts: +### 8. Run the app + +**Local / smoke test** (one terminal): + +```bash +composer dev +``` -- `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 +This starts `php artisan serve`, a queue listener, log tail, and Vite. Open `http://localhost:8000` and sign in with the admin user. -For Inertia SSR development, use `composer dev:ssr` instead. +**Production** — do **not** use `composer dev`. You need long-running processes: -### 6. Production process supervisors +| 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 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). +Full Supervisor configs, Nginx (including Reverb `/app/` + `/apps/`), SSL, and caches: [Production setup](/self-hosting/production). -## Verify the install +## End-to-end checklist -Visit `http://localhost:8000` (or your configured `APP_URL`) and sign in with the admin credentials from step 4. You'll land in the dashboard. +| 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 | -Optional smoke checks: +## Verify -- Create an API key under workspace Settings → API — if Passport keys or the Personal Access Client are missing, this fails with a 500 -- Upload a media file — if you use `FILESYSTEM_DISK=public` and skipped `storage:link`, media URLs 404 +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 a8bec9c..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 @@ -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 ac28f65..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 on bare metal / a VM. If you prefer containers, use [`compose.prod.yaml`](/self-hosting/docker#production-recommended-for-self-hosting) instead — it already runs Horizon, Reverb, and the scheduler for you. +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 @@ -26,32 +38,7 @@ BROADCAST_CONNECTION=reverb FILESYSTEM_DISK=public ``` -See [Configuration](/self-hosting/configuration) for the full variable list. - -## One-time bootstrap - -After migrations, run these once on a fresh server (see [Installation](/self-hosting/installation)): - -```bash -php artisan passport:keys -php artisan db:seed -php artisan db:seed --class=UserSeeder -``` - -Only if you use local media storage (`FILESYSTEM_DISK=public`): - -```bash -php artisan storage:link -``` - -Skip that command when media lives on S3, R2, Spaces, or another object store. - -| Step | Why | -|------|-----| -| `passport:keys` | RSA key pair to sign/verify API and MCP tokens | -| `db:seed` | Passport Personal Access Client + plan rows | -| `UserSeeder` | First admin account | -| `storage:link` | Only for `FILESYSTEM_DISK=public` — not needed for S3 / R2 / Spaces | +See [Configuration](/self-hosting/configuration) for mail, object storage, and social OAuth. ## Optimizations @@ -280,15 +267,16 @@ 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` / `CACHE_STORE` | Queues, cache, schedule locks | +| **Redis** | `QUEUE_CONNECTION=redis` + Redis up | Queues, cache, schedule locks | | **Mail** | SMTP or SendKit | Invites, password reset, alerts | -| **Horizon** | `php artisan horizon` | Processes queues | -| **Reverb** | `php artisan reverb:start` | Real-time WebSocket updates | -| **Scheduler** | `php artisan schedule:run` (cron) | Posts, automations, token refresh | +| **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 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.