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
|
||||
{
|
||||
$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 {
|
||||
$row = DB::table('app_settings')->where('key', $key)->value('value');
|
||||
} catch (QueryException $e) {
|
||||
// The gate reads a setting on every request, so an unreachable
|
||||
// or not-yet-migrated table would take the whole site down —
|
||||
// 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', [
|
||||
'key' => $key, 'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return '__missing__';
|
||||
return $default;
|
||||
}
|
||||
|
||||
// Sentinel: distinguishes "stored null" from "never stored", so a
|
||||
// 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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,6 +133,9 @@ else
|
|||
set_env APP_KEY "base64:$(rand_b64 32)"
|
||||
|
||||
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 REVERB_APP_ID "clupilot"
|
||||
|
|
|
|||
|
|
@ -68,6 +68,14 @@ after="$(git rev-parse HEAD)"
|
|||
if ! git diff --quiet "$before" "$after" -- docker/ 2>/dev/null; then
|
||||
log "Rebuilding the image"
|
||||
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
|
||||
|
||||
# vendor/ and node_modules/ live in the bind mount, so they shadow whatever the
|
||||
|
|
|
|||
Loading…
Reference in New Issue