Spaces:
Running
Running
| # ============================================================================ | |
| # 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 | |
| export SYNC_INTERVAL="${SYNC_INTERVAL:-3600}" # 60 minutes (override with SYNC_INTERVAL secret) | |
| 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' \ | |
| -e '[โโโโโโคโโโผ]|_\\/+_|\-{10,}' \ | |
| -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" | |