fix(deploy): recreate the container, keep the root password secret, forget failures
- update.sh recreated the app container only at the very end, so an update that changes the PHP runtime would have installed dependencies and migrated inside the old one. - install.sh randomised DB_PASSWORD but left DB_ROOT_PASSWORD at the value committed in .env.example — the same root credential on every installation, reachable from any container on the compose network. - Settings cached the fallback after a database failure, so one blip could leave a hidden site publicly visible until someone cleared the cache. Only a successful read is cached now, proven by a test that drops the table and brings it back. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
773ef4bd5f
commit
0219e3c987
|
|
@ -20,25 +20,32 @@ final class Settings
|
||||||
|
|
||||||
public static function get(string $key, mixed $default = null): mixed
|
public static function get(string $key, mixed $default = null): mixed
|
||||||
{
|
{
|
||||||
$raw = Cache::rememberForever(self::CACHE_PREFIX.$key, function () use ($key) {
|
$cacheKey = self::CACHE_PREFIX.$key;
|
||||||
|
|
||||||
|
if (Cache::has($cacheKey)) {
|
||||||
|
$raw = Cache::get($cacheKey);
|
||||||
|
} else {
|
||||||
try {
|
try {
|
||||||
$row = DB::table('app_settings')->where('key', $key)->value('value');
|
$row = DB::table('app_settings')->where('key', $key)->value('value');
|
||||||
} catch (QueryException $e) {
|
} catch (QueryException $e) {
|
||||||
// The gate reads a setting on every request, so an unreachable
|
// The gate reads a setting on every request, so an unreachable
|
||||||
// or not-yet-migrated table would take the whole site down —
|
// or not-yet-migrated table would take the whole site down —
|
||||||
// including during a deploy where new code runs before migrate.
|
// including during a deploy where new code runs before migrate.
|
||||||
// Fall back to the caller's default instead, and say so.
|
// Fall back to the caller's default, but do NOT cache that:
|
||||||
|
// caching a failure would keep a hidden site publicly visible
|
||||||
|
// long after the database came back.
|
||||||
Log::warning('app_settings unavailable, using default', [
|
Log::warning('app_settings unavailable, using default', [
|
||||||
'key' => $key, 'error' => $e->getMessage(),
|
'key' => $key, 'error' => $e->getMessage(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return '__missing__';
|
return $default;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sentinel: distinguishes "stored null" from "never stored", so a
|
// Sentinel: distinguishes "stored null" from "never stored", so a
|
||||||
// missing row does not get re-queried on every request.
|
// missing row does not get re-queried on every request.
|
||||||
return $row === null ? '__missing__' : $row;
|
$raw = $row ?? '__missing__';
|
||||||
});
|
Cache::forever($cacheKey, $raw);
|
||||||
|
}
|
||||||
|
|
||||||
return $raw === '__missing__' ? $default : json_decode($raw, true);
|
return $raw === '__missing__' ? $default : json_decode($raw, true);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -133,6 +133,9 @@ else
|
||||||
set_env APP_KEY "base64:$(rand_b64 32)"
|
set_env APP_KEY "base64:$(rand_b64 32)"
|
||||||
|
|
||||||
set_env DB_PASSWORD "$(rand_hex 24)"
|
set_env DB_PASSWORD "$(rand_hex 24)"
|
||||||
|
# Otherwise every installation shares the value from .env.example, reachable
|
||||||
|
# from any container on the compose network.
|
||||||
|
set_env DB_ROOT_PASSWORD "$(rand_hex 24)"
|
||||||
set_env VPN_CONFIG_KEY "$(rand_b64 32)"
|
set_env VPN_CONFIG_KEY "$(rand_b64 32)"
|
||||||
|
|
||||||
set_env REVERB_APP_ID "clupilot"
|
set_env REVERB_APP_ID "clupilot"
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,14 @@ after="$(git rev-parse HEAD)"
|
||||||
if ! git diff --quiet "$before" "$after" -- docker/ 2>/dev/null; then
|
if ! git diff --quiet "$before" "$after" -- docker/ 2>/dev/null; then
|
||||||
log "Rebuilding the image"
|
log "Rebuilding the image"
|
||||||
docker compose build --quiet app
|
docker compose build --quiet app
|
||||||
|
# Recreate now, not at the end: everything below runs INSIDE this container,
|
||||||
|
# and an update that changes the PHP runtime would otherwise install and
|
||||||
|
# migrate under the old one.
|
||||||
|
docker compose up -d --force-recreate app
|
||||||
|
for _ in $(seq 1 30); do
|
||||||
|
in_app php -v >/dev/null 2>&1 && break
|
||||||
|
sleep 2
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# vendor/ and node_modules/ live in the bind mount, so they shadow whatever the
|
# vendor/ and node_modules/ live in the bind mount, so they shadow whatever the
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue