Refactor configuration handling: implement dynamic loading of config and cache paths for improved security and flexibility in Docker environments

This commit is contained in:
David Dyer committed 2026-10-25 16:49:43 -04:00
1 parent c7b836bcf2
commit bb65ecd43c
7 files changed
+53 -12

No files matched your search

+17 -4
View File
@@ -5,10 +5,13 @@
# docker-compose.yaml (this file) # docker-compose.yaml (this file)
# .env (optional, see PROXY_NETWORK below) # .env (optional, see PROXY_NETWORK below)
# www/ (contents of `npm run dist`) # www/ (contents of `npm run dist`)
# data/ (fitz.sqlite — never inside www/) # config/config.php (live secrets — never inside www/, never in dist/)
# data/ (fitz.sqlite + cache/ — never inside www/)
# #
# First run: create data/ and `chown 33:33 data` (www-data in the image), # First run: create data/ and `chown 33:33 data` (www-data in the image).
# then create the first admin with bin/create-user.php (see its header). # Copy config.example.php to config/config.php, fill it in, and make it
# readable by www-data (`chown root:33` + `chmod 640`). Then create the
# first admin with bin/create-user.php (see its header).
# #
# The CasaOS dev host keeps its own copy of this file in the CasaOS app # The CasaOS dev host keeps its own copy of this file in the CasaOS app
# settings, with CasaOS paths and its x-casaos block. # settings, with CasaOS paths and its x-casaos block.
@@ -43,13 +46,23 @@ services:
- type: bind - type: bind
source: ./www source: ./www
target: /var/www/html target: /var/www/html
# Database lives outside the web root so Apache can't serve it. # Database and cache live outside the web root so Apache can't serve
# them and deploys to www/ never touch them.
- type: bind - type: bind
source: ./data source: ./data
target: /var/www/data target: /var/www/data
# Config is mounted as a directory, not a single file: a single-file
# bind mount pins the inode, so an editor that saves by replacing the
# file leaves the container reading the old copy until a restart.
- type: bind
source: ./config
target: /var/www/config
read_only: true
environment: environment:
- TZ=America/Indiana/Indianapolis - TZ=America/Indiana/Indianapolis
- FITZ_DB_PATH=/var/www/data/fitz.sqlite - FITZ_DB_PATH=/var/www/data/fitz.sqlite
- FITZ_CACHE_DIR=/var/www/data/cache
- FITZ_CONFIG_PATH=/var/www/config/config.php
# Cap container logs so PHP's error log can't slowly fill the disk. # Cap container logs so PHP's error log can't slowly fill the disk.
logging: logging:
driver: json-file driver: json-file
+5 -2
View File
@@ -20,7 +20,11 @@ function fritz_google_reviews(): array {
return $fallback; return $fallback;
} }
$cacheFile = __DIR__ . '/../cache/google-reviews.json'; // In Docker, FITZ_CACHE_DIR points into the writable data/ mount, outside
// the web root, so deploys don't wipe it and www/ can stay read-only to
// www-data. Local dev falls back to cache/ in the project root.
$cacheDir = getenv('FITZ_CACHE_DIR') ?: __DIR__ . '/../cache';
$cacheFile = $cacheDir . '/google-reviews.json';
if (is_file($cacheFile)) { if (is_file($cacheFile)) {
$cached = json_decode((string) file_get_contents($cacheFile), true); $cached = json_decode((string) file_get_contents($cacheFile), true);
@@ -33,7 +37,6 @@ function fritz_google_reviews(): array {
$fresh = fritz_fetch_google_reviews($fallback); $fresh = fritz_fetch_google_reviews($fallback);
$fresh['fetched_at'] = time(); $fresh['fetched_at'] = time();
$cacheDir = dirname($cacheFile);
if (!is_dir($cacheDir)) { if (!is_dir($cacheDir)) {
@mkdir($cacheDir, 0775, true); @mkdir($cacheDir, 0775, true);
} }
+11
View File
@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
/**
* Loads the site config. In Docker, FITZ_CONFIG_PATH points outside the
* web root (see docker-compose.yaml), so a deploy that replaces www/ can
* never overwrite or ship it. Local dev falls back to config.php in the
* project root.
*/
require getenv('FITZ_CONFIG_PATH') ?: __DIR__ . '/../config.php';
+1 -1
View File
@@ -1,5 +1,5 @@
<?php <?php
require __DIR__ . '/config.php'; require __DIR__ . '/inc/load-config.php';
require __DIR__ . '/inc/assets.php'; require __DIR__ . '/inc/assets.php';
require __DIR__ . '/inc/security-headers.php'; require __DIR__ . '/inc/security-headers.php';
send_security_headers(); send_security_headers();
+1 -1
View File
@@ -1,7 +1,7 @@
<?php <?php
declare(strict_types=1); declare(strict_types=1);
require __DIR__ . '/config.php'; require __DIR__ . '/inc/load-config.php';
require __DIR__ . '/inc/assets.php'; require __DIR__ . '/inc/assets.php';
require __DIR__ . '/inc/security-headers.php'; require __DIR__ . '/inc/security-headers.php';
send_security_headers(); send_security_headers();
+8 -4
View File
@@ -6,9 +6,14 @@
* *
* This is an allowlist by design: only the files and folders listed below * This is an allowlist by design: only the files and folders listed below
* are copied. Dev-only stuff (node_modules/, src/, package*.json, .git/, * are copied. Dev-only stuff (node_modules/, src/, package*.json, .git/,
* .claude/, docker-compose.yaml, dev.sh, .hintrc, config.example.php, the * .claude/, docker-compose.yaml, dev.sh, .hintrc, config.php,
* live db/fitz.sqlite, ...) never ends up in dist/ — including anything * config.example.php, the live db/fitz.sqlite, ...) never ends up in
* added later, since it has to be added here explicitly first. * dist/ — including anything added later, since it has to be added here
* explicitly first.
*
* config.php is deliberately excluded: it holds live secrets and lives
* outside the web root on the server (see FITZ_CONFIG_PATH in
* docker-compose.yaml), so a deploy can never overwrite it.
* *
* Run with: npm run dist * Run with: npm run dist
*/ */
@@ -25,7 +30,6 @@ const FILES = [
'main.js', 'main.js',
'styles.css', 'styles.css',
'robots.txt', 'robots.txt',
'config.php', // holds the live FB_PAGE_ACCESS_TOKEN — see README before sharing dist/ anywhere but the server
]; ];
const DIRS = ['admin', 'assets', 'bin', 'inc']; const DIRS = ['admin', 'assets', 'bin', 'inc'];
+10
View File
@@ -24,4 +24,14 @@ if (preg_match($blocked, $path)) {
return true; return true;
} }
// Apache (mod_dir) redirects /admin to /admin/; the built-in server just
// serves admin/index.php at /admin. Every relative link on that page then
// resolves against / instead of /admin/ — "menu.php" lands on the public
// menu, "users.php" 404s. Match Apache so dev behaves like production.
if ($path !== '/' && !str_ends_with($path, '/') && is_dir(__DIR__ . '/..' . $path)) {
$query = parse_url($_SERVER['REQUEST_URI'] ?? '', PHP_URL_QUERY);
header('Location: ' . $path . '/' . ($query ? '?' . $query : ''), true, 301);
return true;
}
return false; return false;