Files
T
WorkBuddy df56c2c137 初始化提交:contentm_agent 工作区全量快照
内容分四块:
1、产品规划产出 —— MCN 短视频整合营销工作台的①段五份(1a 需求/1b 竞品/1c 画像/1d 策略/1e 场景)、②段两份(2a 功能/2b 布局)、③段界面(DESIGN.md 契约与令牌表 + mcn-workbench.html 原型 + 实测/会诊/审查三份 + 23 张闸门截图)。
2、开源竞品调研 —— 5 个内容工作台项目的取证原始件与 1b 系列分析文档。
3、参考资料 —— 竞品视频抽帧 1145 张 + 2 个源视频 + 功能点截图。
4、机制侧 —— 协作脚本与状态台账、工作区记忆日志、抽帧/OCR 脚本。

.gitignore 只排运行时日志、脚本备份副本与一次性探针输出,其余按原样入库。
2026-10-08 08:13:02 +08:00

9.7 KiB
Raw Blame History

PostSider — Production Deployment (VPS)

This guide takes a fresh VPS to a running, HTTPS-secured PostSider instance.

Target: a single VPS with ~6 GB free RAM and ~50 GB disk, a domain you control, and Docker installed. The full stack (app, Postgres, Redis, MinIO, Temporal, Elasticsearch) runs via docker-compose.production.yaml. A host-level reverse proxy terminates TLS and forwards to the app.

Internet ──443──> Caddy/nginx (host) ──> 127.0.0.1:5000 (app container nginx)
                                    └──> 127.0.0.1:9000 (MinIO, /storage/*)

1. Prerequisites

On the VPS:

# Docker + compose plugin
curl -fsSL https://get.docker.com | sh
docker compose version   # must print a version

DNS: create an A record app.example.com → <VPS_PUBLIC_IP> (use your domain).


2. Firewall

Expose only SSH and web. The app, database and admin UIs stay bound to 127.0.0.1 and are never reachable from the internet directly.

sudo ufw allow 22/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable

3. Configure environment

Copy the template and fill it in. Never reuse the development .env — it contains weak/dev secrets and NOT_SECURED=true.

cp .env.production.example .env.production
# Edit .env.production: set your domain + OAuth keys. Leave the CHANGE_ME
# secrets — deploy.sh generates strong random values for them automatically.

Set at minimum:

Variable Value
FRONTEND_URL, BACKEND_URL https://app.example.com
NEXT_PUBLIC_BACKEND_URL https://app.example.com/api
NOT_SECURED leave unset
DISABLE_REGISTRATION true for a private instance
API_LIMIT 60–120

Secrets — leave them as CHANGE_ME... and deploy.sh will generate strong random values automatically, or set them yourself:

openssl rand -base64 64   # JWT_SECRET
openssl rand -base64 32   # ENCRYPTION_KEY
openssl rand -hex 24      # POSTGRES_PASSWORD
openssl rand -hex 32      # MINIO_SECRET_KEY / DBGATE_PASSWORD

ENCRYPTION_KEY is required in production. Without it, stored provider secrets fall back to the weaker legacy AES-256-CBC scheme derived from JWT_SECRET.

Add the OAuth credentials for the social platforms you actually use (X, LinkedIn, Facebook, …). Use fresh production credentials, not the dev keys.


4. Deploy

sudo ./deploy.sh --bootstrap

This will:

  1. Fill any remaining CHANGE_ME secrets (a timestamped backup is kept).
  2. Build the image with NEXT_PUBLIC_* baked in.
  3. Start the full stack and wait until the app is healthy.
  4. Create the first admin user (--bootstrap) — note the one-time password it prints.

Re-deploys / updates:

git pull
sudo ./deploy.sh            # rebuild + restart (migrations run automatically on boot)
sudo ./deploy.sh --no-build # just restart after env-only changes

Existing Postiz-data upgrades

Before the first deploy of this schema to a database that may contain legacy Postiz rows, run this preflight while the old CreationMethod enum still exists. The applied migration 20260628160000_remove_stripped_ai_models removes MCP and AUTOPOST without remapping rows first, so Prisma will abort before any later migration can run if either value remains.

BEGIN;
UPDATE "Post"
SET "creationMethod" = 'API'
WHERE "creationMethod" IN ('MCP', 'AUTOPOST');
COMMIT;

Verify that the update affected the expected rows, then run ./deploy.sh. Fresh installs and databases that never used those creation paths do not need this preflight. Do not edit the applied migration: its Prisma checksum must remain unchanged.

First login: sign in with [email protected] and the one-time password from the bootstrap step, then set your real email and password.


5. Reverse proxy + HTTPS (on the host)

Pick one. Both forward / to the app and /storage/* to MinIO.

sudo cp deploy/Caddyfile /etc/caddy/Caddyfile
sudo nano /etc/caddy/Caddyfile      # set your domain + email
sudo systemctl reload caddy

Option B — nginx + certbot

sudo apt install nginx certbot python3-certbot-nginx
sudo cp deploy/nginx-host.conf /etc/nginx/sites-available/postsider
sudo ln -s /etc/nginx/sites-available/postsider /etc/nginx/sites-enabled/
sudo nano /etc/nginx/sites-available/postsider   # set your domain
sudo nginx -t && sudo systemctl reload nginx
sudo certbot --nginx -d app.example.com

Open https://app.example.com — you should reach the dashboard.


6. Admin UIs (optional, keep them private)

DbGate (127.0.0.1:8082) and Temporal UI (127.0.0.1:8080) are bound to localhost only. Access them over an SSH tunnel:

ssh -L 8080:127.0.0.1:8080 -L 8082:127.0.0.1:8082 user@your-vps

Only expose them publicly behind basic auth (see the commented blocks in deploy/Caddyfile). The MinIO console (:9001) can be removed from the compose ports list in production.


7. Backups

Critical state lives in the Postgres and MinIO volumes. The current helper backs up PostgreSQL only; it does not create a MinIO media backup. Configure an object-storage/volume backup before treating the deployment as disaster- recoverable.

# Database and optional MinIO backup helper
./var/deploy/backup.sh

The helper stores timestamped database backups in /opt/postsider-backups/ and keeps 30 days by default. Install its six-hour cron job with ./var/deploy/setup-backup-cron.sh. Store a copy off-box; the helper's optional MinIO upload requires a configured mc alias.


8. Pre-flight security checklist

  • NOT_SECURED is unset in .env.production
  • JWT_SECRET and ENCRYPTION_KEY are random (not the dev defaults)
  • Strong POSTGRES_PASSWORD, MINIO_SECRET_KEY, DBGATE_PASSWORD
  • DISABLE_REGISTRATION=true (unless you want open sign-up)
  • API_LIMIT set to a sane production value (60–120)
  • Firewall allows only 22/80/443
  • HTTPS works and HTTP redirects to it
  • DbGate / Temporal UI not publicly exposed (or behind auth)
  • Production OAuth keys in use — dev keys from .env rotated/removed
  • MCP_INTROSPECTION_SECRET set (deploy.sh generates it) and the MCP subdomain answers 401 on POST /mcp without a token (see §9)
  • Backups scheduled
  • PostgreSQL backups copied off the VPS
  • MinIO media backup configured and restore-tested

9. Remote MCP server (optional)

PostSider runs a remote MCP server (apps/mcp, container postsider-mcp) that AI clients reach over Streamable HTTP at https://mcp.example.com/mcp. It is what the ChatGPT/Codex directory connects to; Claude and the Codex CLI can too.

  1. DNS: mcp.example.com → <VPS_PUBLIC_IP> (A record).
  2. .env.production: set MCP_PUBLIC_URL, MCP_AUTHORIZATION_SERVER_URL and MCP_API_URL for your domain. MCP_INTROSPECTION_SECRET is generated by deploy.sh and read by BOTH the app and the MCP container — rotating it disconnects every connected client.
  3. Host proxy — nginx: sudo cp deploy/nginx-host-mcp.conf /etc/nginx/sites-available/postsider-mcp, edit the domain, enable it, sudo nginx -t && sudo systemctl reload nginx, then sudo certbot --nginx -d mcp.example.com. Caddy: the mcp.example.com block is already in deploy/Caddyfile.
  4. sudo ./deploy.sh — builds the MCP image and starts the container.

Verify (this is the contract the directory review checks):

curl -s https://mcp.example.com/healthz                                    # {"status":"ok"}
curl -s https://mcp.example.com/.well-known/oauth-protected-resource      # metadata JSON
curl -s -o /dev/null -w '%{http_code}\n' -X POST https://mcp.example.com/mcp   # 401

OAuth clients also need the authorization-server metadata on the AS host (the backend): curl -s https://<MCP_AUTHORIZATION_SERVER_URL>/.well-known/oauth-authorization-server must return the scopes_supported list including openid and email — the OpenAI review rejects a connector whose UserInfo flow cannot advertise those scopes. If the proxy for that host only exposes the backend under /api/, add a root-path passthrough for /.well-known/* (the backend serves the metadata under its own prefix).

A 401 on POST /mcp without a token — with a WWW-Authenticate: Bearer resource_metadata="…" header — is correct: every call is authenticated through OAuth (dynamic client registration → consent screen → PKCE token), and tool calls are forwarded to your API with the user's token only. A 503 means MCP_INTROSPECTION_SECRET is missing on one of the two containers.


Troubleshooting

Symptom Cause / fix
Dashboard loads but every API call fails / hits localhost:3000 NEXT_PUBLIC_BACKEND_URL wasn't set at build time. Set it in .env.production and rebuild with sudo ./deploy.sh.
502 from the reverse proxy App container not healthy yet — docker compose --env-file .env.production -f docker-compose.production.yaml logs -f postsider.
Scheduled posts never publish Orchestrator/Temporal issue — check postsider-temporal and the orchestrator process inside the app container (docker exec postsider-app pm2 ls).
Workers appear healthy but publishing is stuck Check docker exec postsider-app wget -qO- http://127.0.0.1:3002/health/workers and describe the main task queue; publishing requires active main pollers and zero backlog.
Login works locally but not in prod Remove NOT_SECURED from .env.production; any set value enables insecure mode and prevents the production session cookie.
Images don't load (/storage/... 404) Reverse proxy /storage → MinIO mapping missing, or the postsider-media bucket wasn't created.