-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathdocker-entrypoint.sh
More file actions
executable file
·284 lines (261 loc) · 12.2 KB
/
Copy pathdocker-entrypoint.sh
File metadata and controls
executable file
·284 lines (261 loc) · 12.2 KB
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
#!/usr/bin/env bash
# Pinakes Docker entrypoint.
# 1. Generate .env from env vars (idempotent; respects a bind-mounted .env).
# 2. Ensure writable runtime dirs exist on mounted volumes + fix ownership.
# 3. Wait for the database to accept authenticated connections.
# 4. Run the headless installer (as www-data) unless already installed.
# 5. Start and supervise the in-container scheduler (unless disabled).
# 6. Run Apache (or the passed CMD).
set -euo pipefail
APP_DIR=/var/www/html
ENV_FILE="$APP_DIR/.env"
log() { echo "[entrypoint] $*"; }
# --- Defaults (overridable via container env) ------------------------------
: "${DB_HOST:=db}"
: "${DB_PORT:=3306}"
: "${DB_USER:=pinakes}"
: "${DB_PASS:=pinakes}"
: "${DB_NAME:=pinakes}"
: "${DB_SOCKET:=}"
: "${APP_ENV:=production}"
: "${APP_LOCALE:=it_IT}"
: "${APP_CANONICAL_URL:=}"
: "${APP_DEBUG:=false}"
: "${DISPLAY_ERRORS:=false}"
: "${FORCE_HTTPS:=false}"
: "${SESSION_LIFETIME:=3600}"
export DB_HOST DB_PORT DB_USER DB_PASS DB_NAME DB_SOCKET APP_ENV APP_LOCALE
# --- 1. Generate .env ------------------------------------------------------
if [ -f "$ENV_FILE" ] && [ "${PINAKES_FORCE_ENV:-0}" != "1" ]; then
log ".env already present — keeping it (set PINAKES_FORCE_ENV=1 to regenerate)."
else
# PLUGIN_ENCRYPTION_KEY must be stable across restarts. Honour an explicit
# one; otherwise generate base64:<32 random bytes> once.
if [ -z "${PLUGIN_ENCRYPTION_KEY:-}" ]; then
PLUGIN_ENCRYPTION_KEY="base64:$(openssl rand -base64 32)"
log "Generated a new PLUGIN_ENCRYPTION_KEY (persist the .env volume to keep it stable)."
fi
log "Writing $ENV_FILE"
cat > "$ENV_FILE" <<EOF
# Generated by pinakes-docker entrypoint — do not commit.
DB_HOST=${DB_HOST}
DB_USER=${DB_USER}
DB_PASS=${DB_PASS}
DB_NAME=${DB_NAME}
DB_PORT=${DB_PORT}
DB_SOCKET=${DB_SOCKET}
APP_ENV=${APP_ENV}
APP_LOCALE=${APP_LOCALE}
APP_CANONICAL_URL=${APP_CANONICAL_URL}
APP_DEBUG=${APP_DEBUG}
DISPLAY_ERRORS=${DISPLAY_ERRORS}
FORCE_HTTPS=${FORCE_HTTPS}
PLUGIN_ENCRYPTION_KEY=${PLUGIN_ENCRYPTION_KEY}
SESSION_LIFETIME=${SESSION_LIFETIME}
EOF
chown www-data:www-data "$ENV_FILE"
chmod 640 "$ENV_FILE"
fi
# --- 2. Writable runtime dirs (volumes may start empty) --------------------
log "Ensuring writable runtime directories…"
mkdir -p \
"$APP_DIR/storage/sessions" "$APP_DIR/storage/logs" "$APP_DIR/storage/cache" \
"$APP_DIR/storage/backups" "$APP_DIR/storage/calendar" "$APP_DIR/storage/uploads" \
"$APP_DIR/storage/tmp" "$APP_DIR/storage/plugins" "$APP_DIR/storage/rate_limits" \
"$APP_DIR/public/uploads/copertine" "$APP_DIR/public/uploads/autori" \
"$APP_DIR/public/uploads/events" "$APP_DIR/public/uploads/digital" \
"$APP_DIR/public/uploads/archives" \
"$APP_DIR/locale" \
"$APP_DIR/cache" "$APP_DIR/tmp"
# Re-sync the bundled plugins from the image seed (see Dockerfile). Docker only
# populates a named 'storage' volume from the image at FIRST creation, and a bind
# mount hides the image content entirely — so without this an existing volume never
# receives new/updated bundled plugins on image upgrades, and a bind mount loses
# them outright. cp -a (no --delete) overwrites the bundled plugins with the image's
# current copies while leaving any plugins the user installed themselves untouched.
if [ -d /opt/pinakes/storage-seed/plugins ]; then
log "Syncing bundled plugins from image seed…"
cp -a /opt/pinakes/storage-seed/plugins/. "$APP_DIR/storage/plugins/"
fi
if [ -f /opt/pinakes/storage-seed/.htaccess ] && [ ! -f "$APP_DIR/storage/.htaccess" ]; then
cp -a /opt/pinakes/storage-seed/.htaccess "$APP_DIR/storage/.htaccess"
fi
# Re-sync the bundled locale files from the image seed (same rationale as the
# plugins above): the locale/ dir is a persistent volume so custom translations
# survive upgrades, but that means an existing volume never receives new/updated
# shipped translations on an image upgrade. cp -a (no --delete) refreshes the
# shipped top-level *.json while leaving user-added locale files intact.
#
# In-app edits of a SHIPPED locale are written by the app to locale/overrides/
# (never part of the seed), so this refresh never touches them and the app merges
# them back on top at runtime (Pinakes >= 0.7.55). Ensure the dir exists so the
# override survives from first boot even before the first edit.
mkdir -p "$APP_DIR/locale/overrides"
if [ -d /opt/pinakes/locale-seed ]; then
log "Syncing bundled locale files from image seed…"
# Copy only the first-level shipped *.json — never recurse into
# subdirectories, so a persistent locale/overrides/ can never be clobbered.
for locale_file in /opt/pinakes/locale-seed/*.json; do
[ -f "$locale_file" ] || continue
cp -a "$locale_file" "$APP_DIR/locale/"
done
fi
# Ownership: fix storage/uploads (volume mounts come up root-owned). Skip a full
# recursive chown of the (large) app tree on every boot — only the writable bits.
chown -R www-data:www-data \
"$APP_DIR/storage" "$APP_DIR/public/uploads" "$APP_DIR/locale" "$APP_DIR/cache" "$APP_DIR/tmp" 2>/dev/null || true
# --- 3. Wait for the database ----------------------------------------------
if [ -z "$DB_SOCKET" ]; then
log "Waiting for database ${DB_HOST}:${DB_PORT} …"
tries=0
auth_fails=0
# The probe returns: 0 = connected, 2 = access-denied (1045), 1 = transient
# (server not up yet). We fail FAST on persistent auth errors with a clear
# message instead of silently looping for 120s, while still tolerating a
# brief window where the DB user isn't created yet during MySQL init.
while true; do
if php -r '
$h=getenv("DB_HOST");$p=(int)getenv("DB_PORT");$u=getenv("DB_USER");
$w=getenv("DB_PASS");
mysqli_report(MYSQLI_REPORT_OFF);
$c=@mysqli_connect($h,$u,$w,"",$p);
if ($c) { exit(0); }
exit(mysqli_connect_errno() === 1045 ? 2 : 1);
'; then
log "Database is up."
break
else
rc=$?
fi
if [ "$rc" -eq 2 ]; then
auth_fails=$((auth_fails+1))
if [ "$auth_fails" -ge 5 ]; then
log "Database authentication failed (access denied for '${DB_USER}'). Check DB_USER/DB_PASS/DB_NAME."
exit 1
fi
fi
tries=$((tries+1))
if [ "$tries" -ge 60 ]; then
log "Database not reachable at ${DB_HOST}:${DB_PORT} after 120s — giving up."
exit 1
fi
sleep 2
done
fi
# --- 4. Headless install (idempotent) --------------------------------------
run_as_www() {
if command -v runuser >/dev/null 2>&1; then
runuser -u www-data -- "$@"
else
su -p -s /bin/bash www-data -c "$(printf '%q ' "$@")"
fi
}
if [ -f "$APP_DIR/.installed" ]; then
log "Pinakes already installed (.installed present) — skipping installer."
else
log "Running headless installer…"
# Pass the install-time vars explicitly so they survive the user switch.
run_as_www env \
DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_PASS="$DB_PASS" \
DB_NAME="$DB_NAME" DB_SOCKET="$DB_SOCKET" APP_LOCALE="$APP_LOCALE" \
ADMIN_EMAIL="${ADMIN_EMAIL:-}" ADMIN_PASSWORD="${ADMIN_PASSWORD:-}" \
ADMIN_NAME="${ADMIN_NAME:-Admin}" ADMIN_SURNAME="${ADMIN_SURNAME:-User}" \
php /usr/local/lib/pinakes/headless-install.php
fi
# --- 4b. Apply pending DB migrations (image-pull upgrades) ------------------
# A newer image ships newer code AND its migration files, but nothing on Docker
# ran them until now (the in-app updater only covers in-place updates), so
# image-pull upgrades silently accumulated schema drift. The runner drives the
# app's own Updater::runMigrations() and no-ops in milliseconds when the
# recorded schema version already matches the image. Non-fatal by default;
# it exits non-zero only with PINAKES_MIGRATE_STRICT=1.
if [ -f "$APP_DIR/.installed" ]; then
log "Checking for pending database migrations…"
if ! run_as_www env \
DB_HOST="$DB_HOST" DB_PORT="$DB_PORT" DB_USER="$DB_USER" DB_PASS="$DB_PASS" \
DB_NAME="$DB_NAME" DB_SOCKET="$DB_SOCKET" \
PINAKES_MIGRATE_FROM="${PINAKES_MIGRATE_FROM:-}" \
PINAKES_MIGRATE_STRICT="${PINAKES_MIGRATE_STRICT:-}" \
php /usr/local/lib/pinakes/docker-migrate.php; then
log "Migration runner failed with PINAKES_MIGRATE_STRICT=1 — aborting."
exit 1
fi
fi
# --- 4c. Scheduler (supercronic) -------------------------------------------
# Docker has no cron daemon, so the automatic email/mobile push notifications
# (cron/automatic-notifications.php) and nightly maintenance
# (cron/full-maintenance.php) never fire on their own — the #1 reason a Docker
# deployment "never sends any notifications". supercronic reads /etc/pinakes/crontab and
# runs the jobs on schedule, logging to the container's stdout/stderr (visible in
# `docker logs`). It runs as www-data so the cron scripts share the web user's
# ownership of storage/ and .env. The final supervisor below treats it as a required
# process: an unexpected scheduler exit stops the main process and lets Docker's
# restart policy recover the whole container instead of serving indefinitely with
# no automatic notifications. Set TZ (compose/env) so the schedule matches the library's
# local time. Disable with PINAKES_CRON_DISABLED=1 only when an external scheduler
# owns these jobs.
scheduler_pid=""
if [ "${PINAKES_CRON_DISABLED:-0}" = "1" ]; then
log "Scheduler disabled (PINAKES_CRON_DISABLED=1) — automatic notifications/maintenance will NOT run in-container."
elif [ -x /usr/local/bin/supercronic ] && [ -f /etc/pinakes/crontab ]; then
log "Starting supercronic scheduler (TZ=${TZ:-UTC}); jobs log to container stdout."
run_as_www env TZ="${TZ:-UTC}" /usr/local/bin/supercronic -passthrough-logs /etc/pinakes/crontab &
scheduler_pid=$!
else
log "supercronic binary or crontab missing — refusing to start without automatic notifications (set PINAKES_CRON_DISABLED=1 only if scheduled externally)."
exit 1
fi
# Port reminder: EXPOSE 80 is only metadata — a `docker run` without -p reaches
# nothing. The host-side mapping isn't visible from inside the container, so show
# the example (docker compose already maps ${HTTP_PORT:-8080}:80).
cat <<'BANNER'
[entrypoint] ───────────────────────────────────────────────────────────────
[entrypoint] Pinakes is listening on container port 80.
[entrypoint] Map it to a host port to reach it, e.g.:
[entrypoint] docker run -p 8080:80 … -> http://localhost:8080
[entrypoint] (docker compose already maps the HTTP_PORT you set, default 8080)
[entrypoint] ───────────────────────────────────────────────────────────────
BANNER
log "Starting: $*"
# With the scheduler explicitly disabled there is only one long-running process,
# so preserve the usual container behaviour and make the requested command PID 1.
if [ -z "$scheduler_pid" ]; then
exec "$@"
fi
# Bash remains PID 1 only when it has two required children to supervise. Forward
# stop signals to both, reap them, and fail the container if supercronic exits while
# the application is still running. docker-compose.yml's restart policy then starts
# a fresh scheduler and application together.
"$@" &
app_pid=$!
shutdown_requested=0
forward_shutdown() {
shutdown_requested=1
kill -TERM "$app_pid" "$scheduler_pid" 2>/dev/null || true
}
trap forward_shutdown TERM INT
set +e
wait -n "$app_pid" "$scheduler_pid"
first_status=$?
set -e
if [ "$shutdown_requested" -eq 1 ]; then
set +e
wait "$app_pid" 2>/dev/null
wait "$scheduler_pid" 2>/dev/null
set -e
exit 0
fi
if ! kill -0 "$scheduler_pid" 2>/dev/null; then
log "Scheduler exited unexpectedly — stopping the application so Docker can restart the container."
kill -TERM "$app_pid" 2>/dev/null || true
set +e
wait "$app_pid" 2>/dev/null
set -e
exit 1
fi
log "Main process exited with status ${first_status} — stopping the scheduler."
kill -TERM "$scheduler_pid" 2>/dev/null || true
set +e
wait "$scheduler_pid" 2>/dev/null
set -e
exit "$first_status"