内容分四块: 1、产品规划产出 —— MCN 短视频整合营销工作台的①段五份(1a 需求/1b 竞品/1c 画像/1d 策略/1e 场景)、②段两份(2a 功能/2b 布局)、③段界面(DESIGN.md 契约与令牌表 + mcn-workbench.html 原型 + 实测/会诊/审查三份 + 23 张闸门截图)。 2、开源竞品调研 —— 5 个内容工作台项目的取证原始件与 1b 系列分析文档。 3、参考资料 —— 竞品视频抽帧 1145 张 + 2 个源视频 + 功能点截图。 4、机制侧 —— 协作脚本与状态台账、工作区记忆日志、抽帧/OCR 脚本。 .gitignore 只排运行时日志、脚本备份副本与一次性探针输出,其余按原样入库。
9.7 KiB
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_KEYis required in production. Without it, stored provider secrets fall back to the weaker legacy AES-256-CBC scheme derived fromJWT_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:
- Fill any remaining
CHANGE_MEsecrets (a timestamped backup is kept). - Build the image with
NEXT_PUBLIC_*baked in. - Start the full stack and wait until the app is healthy.
- 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.
Option A — Caddy (automatic HTTPS, recommended)
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_SECUREDis unset in.env.productionJWT_SECRETandENCRYPTION_KEYare random (not the dev defaults)- Strong
POSTGRES_PASSWORD,MINIO_SECRET_KEY,DBGATE_PASSWORD DISABLE_REGISTRATION=true(unless you want open sign-up)API_LIMITset 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
.envrotated/removed MCP_INTROSPECTION_SECRETset (deploy.sh generates it) and the MCP subdomain answers401onPOST /mcpwithout 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.
- DNS:
mcp.example.com → <VPS_PUBLIC_IP>(A record). .env.production: setMCP_PUBLIC_URL,MCP_AUTHORIZATION_SERVER_URLandMCP_API_URLfor your domain.MCP_INTROSPECTION_SECRETis generated bydeploy.shand read by BOTH the app and the MCP container — rotating it disconnects every connected client.- 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, thensudo certbot --nginx -d mcp.example.com. Caddy: themcp.example.comblock is already indeploy/Caddyfile. 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. |