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"