File size: 8,548 Bytes
5ec8557
 
 
 
 
 
 
 
 
36f3df7
 
 
 
 
 
 
5ec8557
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36f3df7
5ec8557
 
36f3df7
5ec8557
36f3df7
 
 
 
 
 
 
5ec8557
36f3df7
 
 
5ec8557
36f3df7
5ec8557
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36f3df7
 
5ec8557
 
 
 
0497dac
 
 
5ec8557
36f3df7
5ec8557
36f3df7
5ec8557
36f3df7
5ec8557
36f3df7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5ec8557
36f3df7
 
 
f8a7f2f
 
 
 
 
 
 
 
 
 
 
 
 
 
36f3df7
5ec8557
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407d645
 
 
 
 
 
 
 
5ec8557
 
 
 
36f3df7
5ec8557
1e6332d
 
 
 
 
 
 
 
6d2d3c8
f8a7f2f
 
 
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
#!/bin/bash
set -e

BOOT_START=$(date +%s)

echo "[entrypoint] mem0 Self-Hosted on HuggingFace Spaces"
echo "==============================================================================="

# ── Configuration ───────────────────────────────────────────────────────────
# Affine-style: PGDATA lives DIRECTLY on the HF persistent volume (/data).
# No restore-rsync, no backup loop β€” both hammered the FUSE mount until it
# died ("Transport endpoint is not connected"). FUSE quirks handled below:
# empty dirs are dropped and .git/ is injected into tracked dirs, so dirs
# Postgres scans as object stores must be purged at every boot.
MEM0_WORK="/opt/data"                    # Fast local FS: history, sessions, logs
PGDATA="/data/pgdata"                    # Persistent HF bucket (FUSE)
MEM0_UID=1000
MEM0_GID=1000

# ── Singleton guard ──────────────────────────────────────────────────────────
# Hanya satu instance yang menjalankan steps. Instance duplikat (dari HF rapid-
# restart) cukup sleep infinity β€” tidak exit (exit = HF restart lagi).
# MEM0_ENTRYPOINT_OWNER di-export sebelum exec gosu sehingga instance setelah
# privilege-drop tahu dia adalah kelanjutan sah, bukan duplikat.
ENTRYPOINT_PID_FILE="/tmp/mem0-entrypoint.pid"
ENTRYPOINT_LOG="/tmp/mem0-entrypoint.log"

# Always clear stale PID file at container startup (new container = new PID namespace)
rm -f "$ENTRYPOINT_PID_FILE" "$ENTRYPOINT_LOG" 2>/dev/null || true

if [ -z "${MEM0_ENTRYPOINT_OWNER:-}" ]; then
  # Instance baru tanpa token β†’ cek apakah ada instance lain yang sudah running
  if [ -f "$ENTRYPOINT_PID_FILE" ]; then
    OLD_PID=$(cat "$ENTRYPOINT_PID_FILE" 2>/dev/null || echo "")
    if [ -n "$OLD_PID" ] && kill -0 "$OLD_PID" 2>/dev/null; then
      echo "[entrypoint] Instance $OLD_PID already running β€” standing by silently."
      sleep infinity
      exit 0
    else
      echo "[entrypoint] Stale PID $OLD_PID (dead) β€” taking over..."
      rm -f "$ENTRYPOINT_PID_FILE" "$ENTRYPOINT_LOG" 2>/dev/null || true
    fi
  fi
  echo $$ > "$ENTRYPOINT_PID_FILE"
  export MEM0_ENTRYPOINT_OWNER=$$
  # Buat log world-writable sekarang (sebagai root) agar mem0 bisa write setelah gosu
  touch "$ENTRYPOINT_LOG" && chmod 666 "$ENTRYPOINT_LOG"
fi

exec > >(tee -a "$ENTRYPOINT_LOG") 2>&1

# ── Stage 1: Prepare persistent storage (as root) ───────────────────────────
if [ "$(id -u)" = "0" ]; then
  echo ""
  echo "=== 1. Preparing persistent storage ==="

  mkdir -p "$MEM0_WORK"/{history,sessions,logs}
  mkdir -p "$PGDATA"
  chown ${MEM0_UID}:${MEM0_GID} "$MEM0_WORK"/* 2>/dev/null || true
  chown -R ${MEM0_UID}:${MEM0_GID} "$PGDATA"

  # Init if empty
  FRESH_INIT=0
  if [ ! -f "$PGDATA/PG_VERSION" ]; then
    FRESH_INIT=1
    echo "[entrypoint] Initializing PostgreSQL at $PGDATA"
    chmod 700 "$PGDATA"
    gosu mem0 /usr/lib/postgresql/15/bin/initdb -D "$PGDATA" --auth=trust --encoding=UTF8 --locale=C.UTF-8

    # Configure PostgreSQL for local connections
    cat >> "$PGDATA/postgresql.conf" <<EOF
listen_addresses = 'localhost'
port = 5432
max_connections = 100
shared_buffers = 128MB
effective_cache_size = 512MB
maintenance_work_mem = 64MB
checkpoint_completion_target = 0.9
wal_buffers = 16MB
default_statistics_target = 100
random_page_cost = 1.1
effective_io_concurrency = 200
work_mem = 4MB
min_wal_size = 1GB
max_wal_size = 4GB
EOF

    # Allow local connections without password (incl. replication for future use)
    cat > "$PGDATA/pg_hba.conf" <<EOF
local   all             all                                     trust
host    all             all             127.0.0.1/32            trust
host    all             all             ::1/128                 trust
local   replication     all                                     trust
host    replication     all             127.0.0.1/32            trust
host    replication     all             ::1/128                 trust
EOF

    chown -R ${MEM0_UID}:${MEM0_GID} "$PGDATA"
    echo "[TIMER] PostgreSQL init complete"
  else
    echo "PostgreSQL data exists at $PGDATA"
  fi

  # FUSE repair, every boot (mirrors affine bootstrap.sh):
  # - HF bucket drops empty dirs and injects .git/ into tracked dirs.
  # - Postgres scans these dirs as object stores; any stray entry β†’ PANIC.
  #   They must exist and be COMPLETELY EMPTY (no .gitkeep).
  for dir in pg_replslot pg_twophase pg_dynshmem pg_notify; do
    mkdir -p "$PGDATA/$dir"
    find "$PGDATA/$dir" -mindepth 1 -delete 2>/dev/null || true
  done
  # These tolerate a .gitkeep marker (Postgres ignores unknown files here)
  for dir in pg_commit_ts pg_serial pg_stat pg_stat_tmp \
             pg_logical/snapshots pg_logical/mappings; do
    mkdir -p "$PGDATA/$dir"
    touch "$PGDATA/$dir/.gitkeep"
    find "$PGDATA/$dir" -mindepth 1 -name '.git' -type d -exec rm -rf {} + 2>/dev/null || true
  done
  # pg_tblspc must be a symlink to tmpfs β€” FUSE drops real empty dirs
  mkdir -p /tmp/pg_tblspc
  rm -rf "$PGDATA/pg_tblspc"
  ln -sfn /tmp/pg_tblspc "$PGDATA/pg_tblspc"

  # Defensive re-assert: only UID 1000 persists on the FUSE idmapped mount.
  # Scoped chown on dirs only β€” recursive chown over FUSE has killed the mount.
  chown ${MEM0_UID}:${MEM0_GID} "$PGDATA" "$PGDATA"/*/ 2>/dev/null || true
  chmod 700 "$PGDATA"

  # Fix socket dir so mem0 can create the unix socket lock file
  mkdir -p /var/run/postgresql
  chown ${MEM0_UID}:${MEM0_GID} /var/run/postgresql
  export FRESH_INIT
fi

# ── Stage 2: First-boot DB setup (fresh cluster only) ──────────────────────
if [ "$(id -u)" = "0" ] && [ "${FRESH_INIT:-0}" = "1" ]; then
  echo ""
  echo "=== 2. First-boot database setup ==="
  
  mkdir -p /var/log/postgresql
  chown -R ${MEM0_UID}:${MEM0_GID} /var/log/postgresql
  
  # FUSE repair before first pg_ctl start (same logic as supervisord wrapper)
  for dir in pg_replslot pg_twophase pg_dynshmem pg_notify; do
    mkdir -p "$PGDATA/$dir"
    find "$PGDATA/$dir" -mindepth 1 -delete 2>/dev/null || true
  done
  for dir in pg_commit_ts pg_serial pg_stat pg_stat_tmp pg_logical/snapshots pg_logical/mappings; do
    mkdir -p "$PGDATA/$dir"
    touch "$PGDATA/$dir/.gitkeep"
    find "$PGDATA/$dir" -mindepth 1 -name .git -type d -exec rm -rf {} + 2>/dev/null || true
  done
  mkdir -p /tmp/pg_tblspc
  rm -rf "$PGDATA/pg_tblspc"
  ln -sfn /tmp/pg_tblspc "$PGDATA/pg_tblspc"

  # Start postgres temporarily to run init-db + migrations, then stop
  echo "Running database initialization script..."
  PGLOG="/tmp/pg-init.log"
  touch "$PGLOG" && chown ${MEM0_UID}:${MEM0_GID} "$PGLOG"
  set +e
  gosu mem0 /usr/lib/postgresql/15/bin/pg_ctl -D "$PGDATA" -l "$PGLOG" -w start
  PG_START_RC=$?
  set -e
  echo "[pg_ctl start exit: $PG_START_RC]"
  echo "=== pg startup log ==="
  cat "$PGLOG"
  echo "=== end pg log ==="
  if [ "$PG_START_RC" -ne 0 ]; then
    echo "[ERROR] PostgreSQL failed to start; aborting"
    exit 1
  fi
  gosu mem0 /init-db.sh

  # Run app-DB migrations (users/settings/etc. in mem0_app) BEFORE first boot.
  # Fresh volumes have no tables; without this, uvicorn crashes at import on
  # any DB access and supervisord puts mem0-api into FATAL.
  echo "=== Running database migrations ==="
  gosu mem0 sh -c 'cd /opt/mem0/api && python -m alembic upgrade head' \
    || echo "[WARN] alembic failed (non-fatal; API will retry tables lazily)"

  gosu mem0 /usr/lib/postgresql/15/bin/pg_ctl -D "$PGDATA" -w stop
fi

echo ""
echo "=== 3. Starting services via supervisord ==="

# Stream program logs to container stdout so tracebacks are visible in HF logs.
# (supervisord itself cannot reopen /dev/fd/1 -> EACCES on every dispatcher)
touch /var/log/supervisor/mem0-api.log /var/log/supervisor/mem0-api.err.log \
      /var/log/supervisor/mem0-dashboard.log /var/log/supervisor/mem0-dashboard.err.log \
      /var/log/supervisor/postgresql.log /var/log/supervisor/postgresql.err.log \
      /var/log/supervisor/nginx.log /var/log/supervisor/nginx.err.log
chmod 666 /var/log/supervisor/*.log
tail -q -F /var/log/supervisor/*.log &

# supervisord runs as root (user=root in supervisord.conf) so it can recreate
# FUSE-volatile dirs before each postgres spawn via the pg wrapper command.
exec supervisord -c /etc/supervisor/conf.d/supervisord.conf