feat(deploy): make the all-in-one image installable without a shell (#1124)
The all-in-one image could not be installed from a GUI at all — the deployment it
exists for. validateEnv treats a missing JWT_SECRET as critical and exits, and the
documented run command supplies it with `openssl rand`, a shell command a Synology
Container Manager or QNAP Container Station form cannot run.
wait-for-db.sh now generates one on first start and persists it next to the database,
extending the existing /run/secrets hydration rather than adding a second mechanism.
Explicit env still wins, then /run/secrets, then the generated file. The write is
load-bearing: JWT_SECRET is exported only when the file actually persisted, because an
unpersisted secret would mint a new one every restart and sign every session out.
Creation writes to a private temp file and hard-links it into place — atomic, fails with
EEXIST when another container won, and the loser adopts the winner's value. Non-regular
paths are rejected before the link, since POSIX ln links INTO a directory rather than
failing, which would make a mistyped -v target unrecoverable.
Also repairs the onboarding paths a new install actually walks: the installer no longer
rotates the secrets of a running install on re-run, deprecates the dead scripts/install.sh
in place, corrects the CONTRIBUTING dev loop, and fixes the vite proxy target that had
been pointing at a stray local port since 0da45e69.
Reviewed over three rounds. Co-authored by @Luca-Timo.
This commit is contained in:
@@ -1,6 +1,30 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
# DEPRECATED — do not use. Superseded by scripts/picpeak-setup.sh.
|
||||
#
|
||||
# This script predates the current deployment layout and no longer works. It
|
||||
# refers to two files that do not exist (docker-compose.prod.yml and
|
||||
# scripts/setup-ssl.sh), and its `sed` calls now match the COMMENTED lines in
|
||||
# the current .env.example, so they produce `#JWT_SECRET=<random>` — still
|
||||
# commented, so no secret is ever set. It fails silently rather than loudly,
|
||||
# which is the worst outcome for an installer.
|
||||
#
|
||||
# Nothing in the repository references it. It is kept as a stub only so an old
|
||||
# bookmark or copied command gets a signpost instead of a broken install; the
|
||||
# body below is unreachable and can be deleted outright whenever convenient.
|
||||
cat >&2 <<'DEPRECATED'
|
||||
scripts/install.sh is deprecated and does nothing.
|
||||
|
||||
Use the current installer instead:
|
||||
|
||||
./scripts/picpeak-setup.sh
|
||||
|
||||
Or run the stack directly — see the Quick Start in README.md, or
|
||||
docs/single-container.md for the single-container image.
|
||||
DEPRECATED
|
||||
exit 1
|
||||
|
||||
echo "Photo Sharing Platform - Docker Installation"
|
||||
echo "==========================================="
|
||||
|
||||
|
||||
+128
-13
@@ -317,6 +317,55 @@ generate_password() {
|
||||
openssl rand -base64 32 | tr -d "=+/" | cut -c1-16
|
||||
}
|
||||
|
||||
# Read one KEY=value out of an existing .env, ignoring commented lines. Prints
|
||||
# the empty string when the file or the key is absent, so callers can fall back
|
||||
# to generating a fresh value with `[[ -n "$x" ]] || x=$(generate...)`.
|
||||
# Emit `KEY=value`, or a commented placeholder when the value is empty — an
|
||||
# empty `KEY=` is NOT equivalent: wait-for-db.sh only falls back to
|
||||
# /run/secrets when the variable is unset or empty, but compose would still
|
||||
# define it, and any future reader that treats "defined" as "configured" gets
|
||||
# the wrong answer. Commented out matches what .env.example ships.
|
||||
env_secret_line() {
|
||||
local key="$1" value="$2"
|
||||
if [[ -n "$value" ]]; then
|
||||
printf '%s=%s' "$key" "$value"
|
||||
else
|
||||
printf '# %s is auto-generated into the picpeak-secrets volume on first run.\n#%s=' "$key" "$key"
|
||||
fi
|
||||
}
|
||||
|
||||
read_env_value() {
|
||||
local file="$1" key="$2"
|
||||
[[ -f "$file" ]] || return 0
|
||||
# An unreadable file is NOT the same as a missing key: returning empty here
|
||||
# would regenerate the secrets and then overwrite the file that held them.
|
||||
# Say so loudly instead of silently rotating.
|
||||
if [[ ! -r "$file" ]]; then
|
||||
log_warn "Cannot read $file — existing secrets cannot be reused. Fix its permissions and re-run; the install will stop rather than overwrite it."
|
||||
return 0
|
||||
fi
|
||||
# Tolerant of the shapes a hand-edited .env actually takes — leading
|
||||
# whitespace, an `export` prefix, spaces around the `=`. Reading one of
|
||||
# those as "absent" would regenerate the secrets and then overwrite the
|
||||
# evidence, which is the exact failure this helper exists to prevent.
|
||||
# Never fails: an unreadable file must not abort the installer under
|
||||
# `set -e` mid-run.
|
||||
# Trailing whitespace and a CR are stripped too. A .env edited on Windows
|
||||
# yields `abc\r`; docker compose treats the CR as a line terminator, so the
|
||||
# running stack uses `abc`, but re-emitting `abc\r` mid-line into a fresh
|
||||
# LF file makes it part of the value. The secret then silently differs from
|
||||
# the one in use — every session dies for JWT_SECRET, and Postgres refuses
|
||||
# the connection for DB_PASSWORD. That is the exact outcome this reuse
|
||||
# exists to prevent, so it must not be reachable through it.
|
||||
# Matching surrounding quotes are stripped too. Compose strips them on read,
|
||||
# so `KEY="abc"` round-trips harmlessly there, but the native path is read by
|
||||
# dotenv into the process env — re-emitting `KEY=\"abc\"` would make the
|
||||
# quotes part of the secret on one path and not the other.
|
||||
sed -n -E "s/^[[:space:]]*(export[[:space:]]+)?${key}[[:space:]]*=[[:space:]]*//p" \
|
||||
"$file" 2>/dev/null | head -n1 \
|
||||
| sed -E -e 's/[[:space:]]+$//' -e 's/^"(.*)"$/\1/' -e "s/^'(.*)'\$/\1/" || true
|
||||
}
|
||||
|
||||
generate_jwt_secret() {
|
||||
openssl rand -base64 64 | tr -d "\n"
|
||||
}
|
||||
@@ -589,16 +638,66 @@ setup_docker_installation() {
|
||||
fi
|
||||
|
||||
# Generate machine secrets. Written to .env so they are stable across
|
||||
# restarts; once PR #714's secrets-init service is present it reuses these
|
||||
# exact values (explicit env always wins), so this stays correct either way.
|
||||
local jwt_secret=$(generate_jwt_secret)
|
||||
local db_password=$(generate_password)
|
||||
local redis_password=$(generate_password)
|
||||
# restarts; the compose secrets-init service reuses these exact values
|
||||
# (explicit env always wins), so this stays correct either way.
|
||||
#
|
||||
# Reuse whatever an existing .env already holds. The file is rewritten
|
||||
# wholesale below, so generating unconditionally would rotate the secrets of
|
||||
# an install that is already running: a new JWT_SECRET invalidates every
|
||||
# admin session and every gallery link, and a new DB_PASSWORD no longer
|
||||
# matches the password baked into the initialised Postgres volume, so the
|
||||
# stack stops booting entirely. `--update` has always been the safe path,
|
||||
# but re-running the installer is an easy mistake to make and it should not
|
||||
# cost the user their data access.
|
||||
local jwt_secret db_password redis_password
|
||||
jwt_secret=$(read_env_value "$app_dir/.env" JWT_SECRET)
|
||||
db_password=$(read_env_value "$app_dir/.env" DB_PASSWORD)
|
||||
redis_password=$(read_env_value "$app_dir/.env" REDIS_PASSWORD)
|
||||
|
||||
# Reading nothing back does NOT mean "no secrets exist". The documented
|
||||
# install leaves all three commented out (.env.example) and lets the
|
||||
# compose `secrets-init` service generate them into the picpeak-secrets
|
||||
# volume, which is then the only place the real values live. Writing freshly
|
||||
# generated ones into .env for that install is the worst possible outcome:
|
||||
# secrets-init keeps the OLD values (it never overwrites), Postgres is still
|
||||
# running on the old password, but explicit env now wins in the backend — so
|
||||
# it can no longer authenticate, and JWT_SECRET rotates every session and
|
||||
# gallery link out. Exactly what this reuse exists to prevent.
|
||||
#
|
||||
# So blank stays blank whenever the volume is there to own it. Only a truly
|
||||
# fresh install — no .env value and no volume — gets generated secrets.
|
||||
local secrets_volume_exists="no"
|
||||
if command_exists docker && docker volume ls --format '{{.Name}}' 2>/dev/null \
|
||||
| grep -qE '(^|_)picpeak-secrets$'; then
|
||||
secrets_volume_exists="yes"
|
||||
fi
|
||||
|
||||
if [[ "$secrets_volume_exists" == "yes" ]]; then
|
||||
[[ -n "$jwt_secret" ]] || log_info "JWT_SECRET is managed by the picpeak-secrets volume — leaving it unset."
|
||||
[[ -n "$db_password" ]] || log_info "DB_PASSWORD is managed by the picpeak-secrets volume — leaving it unset."
|
||||
[[ -n "$redis_password" ]] || log_info "REDIS_PASSWORD is managed by the picpeak-secrets volume — leaving it unset."
|
||||
else
|
||||
[[ -n "$jwt_secret" ]] || jwt_secret=$(generate_jwt_secret)
|
||||
[[ -n "$db_password" ]] || db_password=$(generate_password)
|
||||
[[ -n "$redis_password" ]] || redis_password=$(generate_password)
|
||||
fi
|
||||
|
||||
local frontend_port="${CUSTOM_PORT:-3000}"
|
||||
local site_url; site_url="$(base_url)"
|
||||
|
||||
# Create .env for docker-compose.production.yml (prebuilt GHCR images).
|
||||
# The write is wholesale, so anything else the operator hand-edited (extra
|
||||
# PICPEAK_* flags, S3 settings, TZ) is replaced, not merged — reusing the
|
||||
# three secrets above does not make that safe on its own. Keep a copy first,
|
||||
# the same way update_docker_installation already does.
|
||||
if [ -f "$app_dir/.env" ]; then
|
||||
# The rewrite below is wholesale, so losing this copy loses every other
|
||||
# hand-edit in the file. Fail loudly rather than proceeding without it.
|
||||
if ! cp "$app_dir/.env" "$app_dir/.env.backup-$(date +%Y%m%d-%H%M%S)-$$"; then
|
||||
log_error "Could not back up $app_dir/.env before rewriting it — aborting rather than overwriting it."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
log_step "Creating configuration..."
|
||||
cat > "$app_dir/.env" <<EOF
|
||||
# PicPeak Configuration — generated by picpeak-setup.sh on $(date)
|
||||
@@ -611,10 +710,12 @@ COMPOSE_FILE=docker-compose.production.yml
|
||||
PICPEAK_CHANNEL=$PICPEAK_CHANNEL
|
||||
NODE_ENV=production
|
||||
|
||||
# Machine secrets (generated; reused by the compose secrets-init service).
|
||||
JWT_SECRET=$jwt_secret
|
||||
DB_PASSWORD=$db_password
|
||||
REDIS_PASSWORD=$redis_password
|
||||
# Machine secrets. Written explicitly only when this file already pinned them or
|
||||
# there is no picpeak-secrets volume to own them; otherwise left commented so the
|
||||
# secrets-init service stays the single source of truth (see .env.example).
|
||||
$(env_secret_line JWT_SECRET "$jwt_secret")
|
||||
$(env_secret_line DB_PASSWORD "$db_password")
|
||||
$(env_secret_line REDIS_PASSWORD "$redis_password")
|
||||
|
||||
# Database
|
||||
DB_HOST=postgres
|
||||
@@ -837,11 +938,25 @@ setup_native_installation() {
|
||||
log_warn "Frontend directory not found; admin UI will not be served by backend"
|
||||
fi
|
||||
|
||||
# Generate secrets
|
||||
local jwt_secret=$(generate_jwt_secret)
|
||||
|
||||
# Generate secrets — reusing an existing one, for the same reason as the
|
||||
# Docker path above: this heredoc rewrites .env wholesale, so generating
|
||||
# unconditionally would rotate JWT_SECRET on a re-run and sign out every
|
||||
# admin session and gallery link on a live install.
|
||||
local jwt_secret
|
||||
jwt_secret=$(read_env_value "$NATIVE_APP_DIR/app/backend/.env" JWT_SECRET)
|
||||
[[ -n "$jwt_secret" ]] || jwt_secret=$(generate_jwt_secret)
|
||||
|
||||
# Create .env file
|
||||
log_step "Creating configuration..."
|
||||
if [ -f "$NATIVE_APP_DIR/app/backend/.env" ]; then
|
||||
# The rewrite below is wholesale, so losing this copy loses every other
|
||||
# hand-edit in the file. Fail loudly rather than proceeding without it.
|
||||
if ! cp "$NATIVE_APP_DIR/app/backend/.env" \
|
||||
"$NATIVE_APP_DIR/app/backend/.env.backup-$(date +%Y%m%d-%H%M%S)-$$"; then
|
||||
log_error "Could not back up $NATIVE_APP_DIR/app/backend/.env before rewriting it — aborting rather than overwriting it."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
cat > "$NATIVE_APP_DIR/app/backend/.env" <<EOF
|
||||
# PicPeak Native Configuration
|
||||
# Generated: $(date)
|
||||
@@ -1280,7 +1395,7 @@ update_native_installation() {
|
||||
|
||||
# Backup current configuration
|
||||
if [[ -f "$NATIVE_APP_DIR/app/backend/.env" ]]; then
|
||||
cp "$NATIVE_APP_DIR/app/backend/.env" "$NATIVE_APP_DIR/app/backend/.env.backup-$(date +%Y%m%d-%H%M%S)"
|
||||
cp "$NATIVE_APP_DIR/app/backend/.env" "$NATIVE_APP_DIR/app/backend/.env.backup-$(date +%Y%m%d-%H%M%S)-$$"
|
||||
fi
|
||||
|
||||
# Pull latest code
|
||||
|
||||
Reference in New Issue
Block a user