Replaces Buffer with Postiz, a self-hosted open-source social media scheduling tool. Deployed as a single UBI 10 systemd container.
RT #1392
Single container running all services:
| Service | Port | Purpose |
|---|---|---|
| PostgreSQL 16 | 5432 | App DB + Temporal persistence + visibility |
| Redis 7 | 6379 | Caching/sessions |
| Temporal 1.29.3 | 7233 | Workflow orchestration (scheduled posts) |
| Node.js 22 (PM2) | 3000, 4200 | Backend + Frontend |
| nginx | 5000 | Internal reverse proxy |
External: port 8092 -> container port 5000.
podman build -t localhost/postiz:latest .Build requires ~4GB memory for the Node.js compilation step.
The live deployment configuration lives in the private host repo, not here.
postiz-backup.timer runs /usr/local/bin/postiz-backup.sh nightly at 04:00
container-local. It takes a pg_dumpall of the whole cluster — postiz,
temporal, temporal_visibility and the temporal role — gzips it to a dated
file under /root/.backups, keeps 14 days, and refreshes
postiz-latest.sql.gz. The host bind-mounts that directory, and the weekly
/srv sync ships it off-box.
pg_dumpall, not pg_dump: a postiz-only dump restores into a stack that will
not start, because Temporal's schema and role come up missing.
The script refuses to publish a dump it cannot vouch for. It writes to a temp
file, checks gzip integrity, and greps for three cluster markers before the
file is allowed to replace yesterday's — pg_dumpall exits 0 on a connection
it can open but has no rights to read, which yields a well-formed file
containing nothing. A bad dump is worse than a missing one, because a missing
one is visible.
Restore, into a scratch container:
su postgres -c "initdb -D /scratch/pg -A trust"
su postgres -c "pg_ctl -D /scratch/pg -o '-p 5433 -k /tmp' -w start"
gunzip -c postiz-latest.sql.gz | psql -p 5433 -h /tmp -U postgres -d postgresTwo errors are expected and harmless: current user cannot be dropped and
role "postgres" already exists. --clean emits a DROP ROLE for the role
running the restore, which can never succeed. Everything else must be silent.
This is a second layer, not the only one. The host's backup job takes its own
independent pg_dumpall on the weekly and monthly rotations, and Nagios
watches that copy for age and size. This timer exists to cut the worst-case
RPO from seven days to one, and to keep dated history so a dump that goes bad
cannot quietly overwrite the last good one. RT #1495.
Upstream Postiz is built for multi-tenant SaaS. On a single-tenant install the
defaults are badly oversized, and the cost lands mostly outside the V8 heap
where --max-old-space-size cannot reach it. Three levers, in order of impact:
1. Temporal worker fan-out (~700MB). temporal.module.ts registers one
worker per supported provider, not per connected one — 31 workers, each
with its own @temporalio/core-bridge Rust core and thread pair. Set
POSTIZ_ACTIVE_PROVIDERS to the providers you actually use. Confirmed by
thread accounting: 31 workflow-proces + 31 temporal-real-s threads, exactly
one pair per worker.
2. Wrapper processes (~195MB). Upstream launches each app as
pm2 -> pnpm start -> dotenv -> node, costing two extra Node processes per
app. dotenv -e ../../.env reads a file this image never creates, so it is
pure overhead. ecosystem.config.js invokes node directly instead.
3. Heap and pool sizing. Per-app --max-old-space-size via PM2
interpreter_args (a CLI flag beats an inherited NODE_OPTIONS; the resulting
V8 ceiling is the flag + ~48MB). Temporal datastore pools and postgres
max_connections trimmed to match single-tenant reality.
Note that maxCachedWorkflows defaults to a value derived from
v8.getHeapStatistics().heap_size_limit, so the heap cap silently bounds the
sticky workflow cache too. We set it explicitly rather than relying on that
side effect.
Postiz exposes a Public API for scheduling posts. Due to SSE transport issues behind nginx, use the REST API approach:
- Base URL:
https://postiz.crunchtools.com/api/public/v1 - Auth: API key from Postiz Settings UI in
Authorizationheader - Endpoints:
GET /integrations,POST /posts,POST /upload
If building from source on UBI fails, replace the build section in the Containerfile:
# Replace the git clone + pnpm install + pnpm build steps with:
FROM ghcr.io/gitroomhq/postiz-app:v2.19.0 AS postiz-source
# Then in the final stage:
COPY --from=postiz-source /app /app
RUN cd /app && npm rebuild