Spaces:
Running
Running
File size: 17,239 Bytes
b4d7c1c ff3303a b4d7c1c ff3303a 6c0455b b4d7c1c bb56821 b4d7c1c 4a7c78e 264b68d b4d7c1c 72628ff b4d7c1c fd4cae0 b4d7c1c 2aa8665 b4d7c1c 2272bb9 b4d7c1c 2272bb9 b4d7c1c fd4cae0 c63d0d1 866a8e6 c63d0d1 866a8e6 c63d0d1 866a8e6 c63d0d1 866a8e6 b4d7c1c b46eebe b4d7c1c 3d8fd70 b4d7c1c 3d8fd70 b4d7c1c ff3303a b4d7c1c 2272bb9 b4d7c1c 2869840 2272bb9 4a7c78e 2272bb9 2869840 b4d7c1c 2272bb9 b4d7c1c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 | #!/bin/bash
# ============================================================================
# HuggingPost orchestrator
#
# Boot order:
# 1. Compute env (DB_URL, REDIS_URL, FRONTEND_URL, basePath-aware backend URL)
# 2. Persist or generate JWT_SECRET, DB password
# 3. Init Postgres data dir if empty, start postgres, create user + DB
# 4. Start Redis
# 5. Restore DB + uploads + secrets from HF Dataset (if HF_TOKEN set)
# 6. Background: HF Dataset sync loop
# 7. Background: nginx + PM2 (the 4 Postiz procs β same CMD as upstream)
# 8. Foreground: health-server.js on port 7860
# 9. SIGTERM β final sync β graceful exit
# ============================================================================
set -euo pipefail
umask 0077
# ββ Paths ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
POSTIZ_HOME="/postiz"
POSTIZ_DIR="/app"
PGDATA="${POSTIZ_HOME}/pgdata"
SECRETS_DIR="${POSTIZ_HOME}/.secrets"
JWT_SECRET_FILE="${SECRETS_DIR}/jwt-secret"
DB_PASSWORD_FILE="${SECRETS_DIR}/db-password"
mkdir -p "${POSTIZ_HOME}/uploads" "${POSTIZ_HOME}/redis" "${SECRETS_DIR}"
# ββ Public URL βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if [ -n "${SPACE_HOST:-}" ]; then
PUBLIC_URL="https://${SPACE_HOST}"
else
PUBLIC_URL="${PUBLIC_URL:-http://localhost:7860}"
fi
# ββ JWT_SECRET (persist across restarts) βββββββββββββββββββββββββββββββββββββ
if [ -z "${JWT_SECRET:-}" ]; then
if [ -f "${JWT_SECRET_FILE}" ]; then
JWT_SECRET=$(cat "${JWT_SECRET_FILE}")
else
JWT_SECRET=$(openssl rand -base64 48 | tr -d '\n')
printf '%s' "${JWT_SECRET}" > "${JWT_SECRET_FILE}"
chmod 600 "${JWT_SECRET_FILE}"
fi
export JWT_SECRET
fi
# ββ DB password (random hex, persisted) ββββββββββββββββββββββββββββββββββββββ
if [ -f "${DB_PASSWORD_FILE}" ]; then
DB_PASSWORD=$(cat "${DB_PASSWORD_FILE}")
else
DB_PASSWORD=$(openssl rand -hex 24)
printf '%s' "${DB_PASSWORD}" > "${DB_PASSWORD_FILE}"
chmod 600 "${DB_PASSWORD_FILE}"
fi
export PGPASSWORD="${DB_PASSWORD}"
# ββ Postiz env (UI mounted at /app, API at /app/api) ββββββββββββββββββββββββ
# basePath="/app" was patched into apps/frontend/next.config.js at build time,
# so Next.js generates URLs prefixed with /app. NEXT_PUBLIC_BACKEND_URL must
# include /app/api so frontend code calls the right path; health-server
# strips /app before passing to nginx :5000, which then routes /api β backend
# (port 3000) and /uploads β file system.
#
# FRONTEND_URL must be the bare origin (scheme+host, NO /app path suffix).
# The backend uses this for the CORS allow-origin response header. Browsers
# send Origin: https://host (no path), so including /app causes a mismatch
# and blocks every API call (login, signup, etc.).
export DATABASE_URL="${DATABASE_URL:-postgresql://postiz:${DB_PASSWORD}@localhost:5432/postiz}"
export REDIS_URL="${REDIS_URL:-redis://localhost:6379}"
export FRONTEND_URL="${FRONTEND_URL:-${PUBLIC_URL}}"
export MAIN_URL="${MAIN_URL:-${PUBLIC_URL}}"
export NEXT_PUBLIC_BACKEND_URL="${NEXT_PUBLIC_BACKEND_URL:-${PUBLIC_URL}/app/api}"
export BACKEND_INTERNAL_URL="${BACKEND_INTERNAL_URL:-http://localhost:3000}"
export STORAGE_PROVIDER="${STORAGE_PROVIDER:-local}"
export UPLOAD_DIRECTORY="${UPLOAD_DIRECTORY:-${POSTIZ_HOME}/uploads}"
export NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY="${NEXT_PUBLIC_UPLOAD_STATIC_DIRECTORY:-/app/uploads}"
export IS_GENERAL="${IS_GENERAL:-true}"
export NX_ADD_PLUGINS="${NX_ADD_PLUGINS:-false}"
export NODE_ENV="${NODE_ENV:-production}"
# HF Space proxy rewrites Set-Cookie Domain to .hf.space which is a public
# suffix β browsers reject such cookies. NOT_SECURED=true makes the backend
# also send the JWT as an `auth` response header; the frontend JS reads it
# and sets the cookie via document.cookie (no domain attr) so it lands on
# the exact hostname and the browser accepts it.
export NOT_SECURED="${NOT_SECURED:-true}"
# Sync config
# Sanitize: strip non-digits, clamp minimum to 60s to prevent spin loops.
SYNC_INTERVAL=$(printf '%s' "${SYNC_INTERVAL:-3600}" | tr -dc '0-9')
{ [ -z "${SYNC_INTERVAL}" ] || [ "${SYNC_INTERVAL}" -lt 60 ]; } && SYNC_INTERVAL=3600
export SYNC_INTERVAL
export SYNC_MAX_FILE_BYTES="${SYNC_MAX_FILE_BYTES:-524288000}" # 500 MB (default; covers .next + DB + uploads)
export BACKUP_DATASET_NAME="${BACKUP_DATASET_NAME:-huggingpost-backup}"
# ββ Google β YouTube env alias βββββββββββββββββββββββββββββββββββββββββββββββ
# Postiz internally uses YOUTUBE_CLIENT_ID/SECRET for both Google OAuth login
# and YouTube channel integration. Users set the friendlier GOOGLE_CLIENT_ID/
# SECRET; we map them here so Postiz picks them up automatically.
if [ -n "${GOOGLE_CLIENT_ID:-}" ]; then
export YOUTUBE_CLIENT_ID="${GOOGLE_CLIENT_ID}"
export YOUTUBE_CLIENT_SECRET="${GOOGLE_CLIENT_SECRET:-}"
fi
# ββ Banner βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
echo ""
echo " ββββββββββββββββββββββββββββββββββββββ"
echo " β HuggingPost β"
echo " β Postiz on Hugging Face Spaces β"
echo " ββββββββββββββββββββββββββββββββββββββ"
echo ""
echo "Public host : ${SPACE_HOST:-not detected}"
echo "Dashboard : ${PUBLIC_URL}/"
echo "Postiz UI : ${PUBLIC_URL}/app/"
echo "Postiz API : ${PUBLIC_URL}/app/api/"
echo "Sync every : ${SYNC_INTERVAL}s"
echo "HF backup : $([ -n "${HF_TOKEN:-}" ] && echo 'enabled' || echo 'disabled (no HF_TOKEN)')"
echo ""
# ββ Postgres βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
PG_BIN="/usr/libexec/postgresql16"
[ -x "${PG_BIN}/postgres" ] || PG_BIN="/usr/bin"
if [ ! -f "${PGDATA}/PG_VERSION" ]; then
echo "Initializing Postgres cluster at ${PGDATA}..."
chown -R postgres:postgres "${PGDATA}"
su-exec postgres "${PG_BIN}/initdb" -D "${PGDATA}" --locale=C.UTF-8 --encoding=UTF8 >/dev/null
echo "host all all 127.0.0.1/32 scram-sha-256" >> "${PGDATA}/pg_hba.conf"
fi
chown -R postgres:postgres "${PGDATA}"
if ! su-exec postgres "${PG_BIN}/pg_ctl" -D "${PGDATA}" status >/dev/null 2>&1; then
echo "Starting Postgres..."
su-exec postgres "${PG_BIN}/pg_ctl" -D "${PGDATA}" \
-l "/tmp/pg.log" \
-o "-c listen_addresses='127.0.0.1' -c unix_socket_directories='/var/run/postgresql'" \
start >/dev/null
fi
for _ in $(seq 1 30); do
su-exec postgres pg_isready -h 127.0.0.1 >/dev/null 2>&1 && break
sleep 1
done
su-exec postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname='postiz'" | grep -q 1 \
|| su-exec postgres psql -c "CREATE ROLE postiz WITH LOGIN PASSWORD '${DB_PASSWORD}';" >/dev/null
su-exec postgres psql -c "ALTER ROLE postiz WITH PASSWORD '${DB_PASSWORD}';" >/dev/null
su-exec postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname='postiz'" | grep -q 1 \
|| su-exec postgres psql -c "CREATE DATABASE postiz OWNER postiz;" >/dev/null
echo "β Postgres"
# ββ Redis ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
redis-server --daemonize yes \
--bind 127.0.0.1 \
--port 6379 \
--appendonly yes \
--dir "${POSTIZ_HOME}/redis" \
--logfile /tmp/redis.log
for _ in $(seq 1 10); do
redis-cli -h 127.0.0.1 -p 6379 ping 2>/dev/null | grep -q PONG && break
sleep 1
done
echo "β Redis"
# ββ Restore from HF Dataset ββββββββββββββββββββββββββββββββββββββββββββββββββ
if [ -n "${HF_TOKEN:-}" ]; then
echo "Restoring persisted data from HF Dataset..."
python3 /opt/postiz-sync.py restore 2>&1 || true
if [ -f "${DB_PASSWORD_FILE}" ]; then
DB_PASSWORD=$(cat "${DB_PASSWORD_FILE}")
export PGPASSWORD="${DB_PASSWORD}"
export DATABASE_URL="postgresql://postiz:${DB_PASSWORD}@localhost:5432/postiz"
fi
su-exec postgres psql -c "ALTER ROLE postiz WITH PASSWORD '${DB_PASSWORD}';" >/dev/null 2>&1 || true
else
echo "HF_TOKEN not set β running without backup persistence"
echo " Add HF_TOKEN as a Space secret to enable DB+uploads backup."
fi
# ββ Patch next/font/google β next/font/local (runtime safety net) ββββββββββββ
# Docker Stage 1 may be cached from before this patch was introduced.
# Apply here unconditionally so the cached image is fixed at container start.
# No-op if layout.tsx already uses next/font/local (idempotent grep check).
_APP_LAYOUT="${POSTIZ_DIR}/apps/frontend/src/app/(app)/layout.tsx"
if grep -q "next/font/google" "${_APP_LAYOUT}" 2>/dev/null; then
echo "Patching next/font/google β next/font/local (cached image lacks build-time patch)..."
mkdir -p "${POSTIZ_DIR}/apps/frontend/src/fonts"
cp /opt/vendor/fonts/*.woff2 "${POSTIZ_DIR}/apps/frontend/src/fonts/"
cd "${POSTIZ_DIR}"
node /opt/vendor/patch-jakarta-font.js
cd /
echo "Font patch applied."
else
echo "Font patch: layout.tsx already uses next/font/local β skipping."
fi
# ββ Build Next.js frontend (first boot or after next.config.js change) βββββββ
# next build is NOT run during docker build β the HF builder's ~4 GB cgroup
# limit is less than what next build needs. We run it here where the runtime
# has 16 GB. On subsequent starts the .next directory is restored from the
# HF Dataset backup, so this block only executes once per config version.
#
# Config-hash check: if next.config.js changed (new image deploy), the stored
# hash inside .next won't match β we rebuild automatically even if BUILD_ID
# exists. This avoids serving a .next compiled with stale settings.
FRONTEND_NEXT="${POSTIZ_DIR}/apps/frontend/.next"
CONFIG_HASH=$(md5sum "${POSTIZ_DIR}/apps/frontend/next.config.js" 2>/dev/null | cut -d' ' -f1 || echo "none")
STORED_HASH=$(cat "${FRONTEND_NEXT}/.config-hash" 2>/dev/null || echo "")
if [ ! -f "${FRONTEND_NEXT}/BUILD_ID" ] || [ "${CONFIG_HASH}" != "${STORED_HASH}" ]; then
if [ "${CONFIG_HASH}" != "${STORED_HASH}" ] && [ -f "${FRONTEND_NEXT}/BUILD_ID" ]; then
echo ""
echo " next.config.js changed β rebuilding frontend (~5 min)..."
echo ""
else
echo ""
echo " βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo " β Building Next.js frontend (first boot β takes ~5 min) β"
echo " β Dashboard is live at ${PUBLIC_URL}/ β"
echo " β Postiz will start automatically when the build finishes. β"
echo " βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo ""
fi
cd "${POSTIZ_DIR}"
SENTRY_DSN="" \
SENTRY_AUTH_TOKEN="" \
SENTRY_ORG="" \
SENTRY_PROJECT="" \
NEXT_PUBLIC_SENTRY_DSN="" \
NEXT_TELEMETRY_DISABLED=1 \
NEXT_PRIVATE_SKIP_SIZE_MINIMIZATION=true \
NODE_OPTIONS="--max-old-space-size=8192" \
pnpm run build:frontend 2>&1 | sed 's/^/[frontend-build] /'
echo "${CONFIG_HASH}" > "${FRONTEND_NEXT}/.config-hash"
echo "Frontend build complete."
cd /
fi
# ββ Cloudflare proxy bootstrap βββββββββββββββββββββββββββββββββββββββββββββββ
if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then
echo "Setting up Cloudflare proxy..."
python3 /opt/cloudflare-proxy-setup.py 2>&1 || echo "Cloudflare setup failed; continuing without proxy"
fi
_CF_ENV="/tmp/huggingpost-cloudflare-proxy.env"
if [ -f "${_CF_ENV}" ]; then
# shellcheck source=/dev/null
. "${_CF_ENV}"
fi
if [ -n "${CLOUDFLARE_PROXY_URL:-}" ] && [ -f /opt/cloudflare-proxy.js ]; then
export NODE_OPTIONS="${NODE_OPTIONS:-} --require /opt/cloudflare-proxy.js"
fi
# ββ Cloudflare KeepAlive worker ββββββββββββββββββββββββββββββββββββββββββββββ
if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then
echo "Setting up Cloudflare KeepAlive worker..."
python3 /opt/cloudflare-keepalive-setup.py || true
fi
# ββ Background HF sync loop ββββββββββββββββββββββββββββββββββββββββββββββββββ
SYNC_PID=""
if [ -n "${HF_TOKEN:-}" ]; then
(
sleep 60 # Initial backup 60s after boot to save setup (signup, keys)
while true; do
python3 /opt/postiz-sync.py sync 2>&1 || true
sleep "$SYNC_INTERVAL"
done
) &
SYNC_PID=$!
fi
# ββ Health server (public port 7860) βββββββββββββββββββββββββββββββββββββββββ
node /opt/healthsrv/health-server.js &
HEALTH_PID=$!
sleep 1
# ββ Postiz: nginx + PM2 (mirrors upstream CMD `nginx && pnpm run pm2`) βββββββ
# pm2-run script does: pm2 delete all || true && pnpm run prisma-db-push
# && pnpm run --parallel pm2 && pm2 logs
echo "Starting Postiz..."
cd "${POSTIZ_DIR}"
( nginx && pnpm run pm2 2>&1 | grep -Ev \
-e '\[RoutesResolver\]|\[RouterExplorer\]|Mapped \{|\[InstanceLoader\]' \
-e '\[PM2\] (Spawning|Successfully daemonized|Starting .* fork_mode|Done\.)' \
-e '\[PM2\]\[WARN\] No process' \
-e 'Runtime Edition|Production Process Manager|built-in Load Balancer' \
-e 'Start and Daemonize|Load Balance|Make pm2 auto-boot|To go further' \
-e 'pm2\.io|pm2 monitor|pm2 startup|PM2 log:|pm2 start ' \
-e '\[TAILING\]|/root/\.pm2/logs/' \
-e 'Packages: \+[0-9]|^\+\+\+|preinstall\$|preinstall: Done' \
-e 'Scope: [0-9]+ of|Progress: resolved|\(Use --lines' \
-e '^apps/(frontend|backend|cron|workers) pm2:' \
-e '^> gitroom@|^> postiz-[a-z]|^> pnpm (dlx|run)|^> dotenv|^> pm2 ' \
-e '[ββββββ€βββΌ]|_\\/+_|\-{10,}|\\{4,}' \
-e '/root/\.pm2/.*\.log last [0-9]' \
-e '^[[:space:]]*$' \
| sed 's/^/[postiz] /' ) &
POSTIZ_PID=$!
echo "Waiting for Postiz..."
for i in $(seq 1 90); do
if curl -sf -m 2 http://127.0.0.1:5000/ >/dev/null 2>&1; then
echo "Postiz ready (~$((i*2))s)"
break
fi
sleep 2
done
echo ""
echo " βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo " β HuggingPost is live! β"
echo " β β"
echo " β Dashboard : ${PUBLIC_URL}/"
echo " β Postiz : ${PUBLIC_URL}/app/"
echo " β β"
echo " β Sign up to create the first admin account. β"
echo " βββββββββββββββββββββββββββββββββββββββββββββββββββββββ"
echo ""
# ββ Graceful shutdown ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
cleanup() {
echo "Shutting down β running final sync..."
[ -n "${HEALTH_PID:-}" ] && kill "$HEALTH_PID" 2>/dev/null || true
[ -n "${POSTIZ_PID:-}" ] && kill "$POSTIZ_PID" 2>/dev/null || true
pm2 kill >/dev/null 2>&1 || true
nginx -s quit 2>/dev/null || true
if [ -n "${SYNC_PID:-}" ]; then
kill "$SYNC_PID" 2>/dev/null || true
wait "$SYNC_PID" 2>/dev/null || true
fi
if [ -n "${HF_TOKEN:-}" ]; then
python3 /opt/postiz-sync.py sync 2>&1 || true
fi
redis-cli -h 127.0.0.1 -p 6379 shutdown nosave 2>/dev/null || true
su-exec postgres "${PG_BIN}/pg_ctl" -D "${PGDATA}" stop -m fast 2>/dev/null || true
exit 0
}
trap cleanup SIGTERM SIGINT
wait "$POSTIZ_PID"
|