From 15b32ed256182cce7b744f303bd888535dde4484 Mon Sep 17 00:00:00 2001 From: code Date: Fri, 17 Jul 2026 02:43:03 +0000 Subject: [PATCH] Make Docker bind mounts actually work; refresh homepage feature grid App/, cache/, uploads/, and data/ are now bind-mounted by default, so docker-entrypoint.sh seeds an empty App/ from a build-time backup and re-chowns the mounted paths to http:http on every start (a bind mount doesn't inherit a named volume's ownership or get seeded from the image the way COPY does). Also fixes AllowOverride never actually being enabled (the sed pattern didn't account for httpd.conf's indentation, so only DirectoryIndex-served routes worked) and pins Apache/PHP/SQLite to a dated Arch Linux Archive snapshot for reproducible builds. Homepage's feature grid was six cards behind what's actually shipped; brought it in line with the features blog post. Co-Authored-By: Claude Sonnet 5 --- .gitignore | 1 + App/pages/index.twig | 32 +++++++++++++++-- Dockerfile | 34 +++++++++++++++--- README.md | 12 ++++++- docker-compose.yml | 17 +++------ docker-entrypoint.sh | 29 +++++++++++++++ novaconium/pages/admin/docs/docker/index.twig | 36 +++++++++++-------- novaconium/sass/main.sass | 2 +- public/css/main.css | 18 ++++++++++ 9 files changed, 146 insertions(+), 35 deletions(-) create mode 100644 docker-entrypoint.sh diff --git a/.gitignore b/.gitignore index 00b9a3f..8402f5b 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ /graphify-out/ /public/uploads/* !/public/uploads/.gitkeep +.env \ No newline at end of file diff --git a/App/pages/index.twig b/App/pages/index.twig index 6df0d33..9fa5829 100644 --- a/App/pages/index.twig +++ b/App/pages/index.twig @@ -56,8 +56,36 @@

Meta description, canonical links, Open Graph, and Twitter Card tags ship by default, all overridable per page.

-

Matomo & admin auth

-

Built-in analytics tracking and a multi-user session login for /admin/* — both off until you turn them on in App/config.php.

+

Admin authentication

+

A multi-user session login gates every /admin/* route, with email verification for new accounts. Off by default in App/config.php.

+
+
+

Access control & drafts

+

Gate a page to a user or group with one Lib\Access call in its sidecar, or preview an unfinished page as an admin-only draft.

+
+
+

Media manager & comments

+

An upload/browse/delete UI at /admin/media, and Lib\Comments for a moderated comment thread on any page.

+
+
+

Database, zero setup

+

Lib\Db wraps PDO for SQLite or MySQL, multiple named connections at once, migrating automatically on first use.

+
+
+

Search, sitemap & tags

+

One content index backs full-text /search, /sitemap.xml, and blog tag browsing — reindexed lazily, no extra steps.

+
+
+

Blog RSS feed

+

/blog/feed is built from the same hand-written post list App/pages/blog/index.php renders from — no database required.

+
+
+

Matomo & dark/light theme

+

Built-in analytics tracking, off by default, alongside a nav toggle that swaps every color via CSS custom properties.

+
+
+

Form security by default

+

Lib\Csrf, Lib\SpamGuard's honeypot check, and cleaning input accessors — wired into the contact form and every admin form.

Override anything

diff --git a/Dockerfile b/Dockerfile index 05c3eb1..94ab282 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,20 @@ # Arch Linux + Apache + PHP image for running novaconium in production. -# See /admin/docs/docker for volumes, overriding App/ without a rebuild, -# and optional MySQL wiring. +# See /admin/docs/docker for the bind-mounted paths, docker-entrypoint.sh's +# seeding/permissions behavior, and optional MySQL wiring. FROM archlinux:base -RUN pacman -Syu --noconfirm --needed apache php php-apache sqlite \ +# Arch's repos are rolling-release with no version-pinned packages, so +# `pacman -S apache=X.Y` isn't reliably available — point pacman at a dated +# Arch Linux Archive (https://archive.archlinux.org) snapshot instead of the +# live mirror so a rebuild months from now installs the same Apache/PHP/ +# SQLite versions instead of whatever happens to be current that day. Bump +# ARCH_SNAPSHOT deliberately (e.g. to pick up a PHP security release), not +# as a side effect of an unrelated rebuild. +ARG ARCH_SNAPSHOT=2026/07/01 +RUN echo "Server=https://archive.archlinux.org/repos/${ARCH_SNAPSHOT}/\$repo/os/\$arch" \ + > /etc/pacman.d/mirrorlist + +RUN pacman -Syyu --noconfirm --needed apache php php-apache sqlite \ && pacman -Scc --noconfirm # php-apache on Arch is built against mpm_prefork, not httpd's default @@ -15,7 +26,7 @@ RUN sed -i \ -e '/^#LoadModule rewrite_module/s/^#//' \ -e 's#DocumentRoot "/srv/http"#DocumentRoot "/var/www/html/public"#' \ -e 's###' \ - -e 's/^AllowOverride None/AllowOverride All/' \ + -e 's/^\( *\)AllowOverride None$/\1AllowOverride All/' \ /etc/httpd/conf/httpd.conf \ && printf '\nLoadModule php_module modules/libphp.so\nAddHandler php-script .php\nDirectoryIndex index.php index.html\nServerName localhost\n' \ >> /etc/httpd/conf/httpd.conf \ @@ -30,11 +41,24 @@ COPY novaconium/ ./novaconium/ COPY public/ ./public/ COPY App/ ./App/ -# Runtime-writable paths not covered by named volumes in docker-compose.yml. +# Pristine copy of the starter App/, kept outside /var/www/html so +# docker-entrypoint.sh can reseed a bind-mounted (but empty/missing) App/ on +# first start — see docker-entrypoint.sh and /admin/docs/docker. +RUN cp -a App/ /opt/novaconium-app-default/ + +# Runtime-writable paths — cache/uploads/App/data are bind-mounted from the +# host by docker-compose.yml, so docker-entrypoint.sh re-chowns them at +# every container start (a build-time chown only survives on the image +# layer, not on a host bind mount). This chown still covers a fresh +# container with no bind mounts configured at all. RUN mkdir -p public/cache public/uploads data \ && touch novaconium/contact-log.txt \ && chown -R http:http public/cache public/uploads data App novaconium/contact-log.txt +COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh + EXPOSE 80 +ENTRYPOINT ["docker-entrypoint.sh"] CMD ["httpd", "-D", "FOREGROUND"] diff --git a/README.md b/README.md index 63a7c6c..78fbf61 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,11 @@ For a full tour of what's included — routing, sidecars, caching, admin auth, a ## Getting started -**Requirements:** PHP 8.1+ (uses `readonly` constructor-promoted properties) and, for production, Apache with `mod_rewrite` and `AllowOverride All`. A few optional features (database, content index/search, admin authentication) need the `pdo_sqlite` extension — see `/admin/docs` for details once running. +### Requirements: + +PHP 8.1+ (uses `readonly` constructor-promoted properties) and, for production, Apache with `mod_rewrite` and `AllowOverride All`. A few optional features (database, content index/search, admin authentication) need the `pdo_sqlite` extension — see `/admin/docs` for details once running. + +### Developers Run it locally, no Apache needed: @@ -23,6 +27,12 @@ php -S 127.0.0.1:8000 -t public public/router.php Visit `http://127.0.0.1:8000/` — click around the example pages, then open `http://127.0.0.1:8000/admin/docs` for the complete documentation, rendered live from this same instance. +### Webmasters + +- Clone this repo. +- docker build: ``` docker build -t novaconium . ``` +- docker compose up -d + ## Documentation The full framework documentation lives inside the framework itself, at `/admin/docs` on any running instance — so it travels with the code, no internet connection needed. That's the canonical reference for everything: requirements, running locally, deploying on Apache or Docker, starting a new project, updating the framework, adding a page, routing, sidecars, libraries, database, session, content index, XML sitemap, RSS feeds, layouts, static caching, SEO, Matomo analytics, admin authentication, access control, draft pages, media manager, styling, and project layout. diff --git a/docker-compose.yml b/docker-compose.yml index 635a6c1..0236d17 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,14 +1,13 @@ services: web: - build: . + image: novaconium-beta ports: - "8080:80" volumes: - - cache:/var/www/html/public/cache - - uploads:/var/www/html/public/uploads - - data:/var/www/html/data - # Uncomment to override the baked-in App/ with a host copy, no rebuild: - # - ./App:/var/www/html/App + - ${VOL_PATH:-/data}/novaconium/cache:/var/www/html/public/cache + - ${VOL_PATH:-/data}/novaconium/uploads:/var/www/html/public/uploads + - ${VOL_PATH:-/data}/novaconium/App:/var/www/html/App + - ${VOL_PATH:-/data}/novaconium/data:/var/www/html/data # Optional — only needed if App/config.php adds a db_connections entry # with driver: mysql. See /admin/docs/database. @@ -21,9 +20,3 @@ services: # MYSQL_ROOT_PASSWORD: change-me # volumes: # - mysql-data:/var/lib/mysql - -volumes: - cache: - uploads: - data: - # mysql-data: diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..c14e0e2 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,29 @@ +#!/bin/sh +# Runs once per container start, before Apache — see /admin/docs/docker. +# +# docker-compose.yml bind-mounts App/, public/cache/, public/uploads/, and +# data/ from the host so a project's content/db survive a rebuild and can be +# edited without one. Two problems a plain COPY-at-build-time image can't +# solve on its own: +# +# 1. A bind mount to an empty (or not-yet-created) host directory shadows +# whatever COPY baked into that path in the image, replacing it with +# nothing — Docker does not seed bind mounts from image content the way +# it seeds a fresh named volume. App/ is only ever the docs/starter +# content wanted on host: seed it from the pristine copy stashed +# at build time (/opt/novaconium-app-default) if the mounted dir is +# empty, so `docker compose up` produces a working site on a first run +# with no manual copy step. +# 2. A bind-mounted host directory keeps the host's ownership, not the +# image's — the build-time `chown` in the Dockerfile never applies to +# it. Re-chown the mounted paths to the Apache worker user on every +# start so they're writable regardless of the host-side UID/GID. +set -e + +if [ -z "$(ls -A /var/www/html/App 2>/dev/null)" ]; then + cp -a /opt/novaconium-app-default/. /var/www/html/App/ +fi + +chown -R http:http /var/www/html/public/cache /var/www/html/public/uploads /var/www/html/App /var/www/html/data + +exec "$@" diff --git a/novaconium/pages/admin/docs/docker/index.twig b/novaconium/pages/admin/docs/docker/index.twig index 7cad083..ae5f49d 100644 --- a/novaconium/pages/admin/docs/docker/index.twig +++ b/novaconium/pages/admin/docs/docker/index.twig @@ -2,44 +2,52 @@ {% block title %}Docker{% endblock %} -{% block description %}Running novaconium in a container: the Apache/PHP image, its three volumes, and overriding App/ without a rebuild.{% endblock %} +{% block description %}Running novaconium in a container: the Apache/PHP image, its four bind-mounted paths, and how docker-entrypoint.sh seeds and permissions them.{% endblock %} {% block robots %}noindex, nofollow{% endblock %} {% block docs_content %}

Docker

-

The root Dockerfile builds an Apache + PHP image on an Arch Linux base, with mod_rewrite, AllowOverride All, and pdo_sqlite/pdo_mysql already enabled — nothing else in Getting started's "Deploy on Apache" section needs configuring by hand. docker-compose.yml wires it up with three named volumes so a project's own cache, uploads, and database never live inside a path that gets wiped by the "Updating the framework" workflow.

+

The root Dockerfile builds an Apache + PHP image on an Arch Linux base, with mod_rewrite, AllowOverride All, and pdo_sqlite/pdo_mysql already enabled — nothing else in Getting started's "Deploy on Apache" section needs configuring by hand.

docker compose up --build

Visit http://localhost:8080/.

-

The volumes

+

The bind mounts

+ +

docker-compose.yml bind-mounts four host paths under ${VOL_PATH:-/data}/novaconium/ (override VOL_PATH in the environment to relocate all four at once) so a project's content, uploads, cache, and database live on the host — editable without a rebuild, and untouched by the "Updating the framework" workflow:

    +
  • App/var/www/html/App — the project itself (pages, lib, config, migrations). Edit it on the host; changes show up after docker compose restart web, no rebuild.
  • cachepublic/cache/ — the static HTML page cache (see Static caching). Disposable; clear it from inside the container with docker compose exec web php novaconium/bin/clear-cache.php.
  • -
  • uploadspublic/uploads/ — files uploaded through Media manager (/admin/media). Needs its own volume specifically because public/ is otherwise baked into the image with COPY at build time — without this, an upload would only survive until the next docker compose up --build. Kept separate from the data volume below deliberately, since a project might use MySQL or no database at all and shouldn't have upload storage coupled to the SQLite volume.
  • -
  • datadata/ — holds data/novaconium.sqlite if Database-backed features (admin auth, content index) are enabled. Persists across docker compose down/up as long as you don't pass -v.
  • +
  • uploadspublic/uploads/ — files uploaded through Media manager (/admin/media).
  • +
  • datadata/ — holds data/novaconium.sqlite if Database-backed features (admin auth, content index) are enabled.
-

App/ itself is not a named volume — it's baked into the image with COPY at build time, so docker compose up --build alone produces a working site with no extra steps. A named volume seeded from COPY App/ would only populate once, on first container creation, and would silently go stale on every later rebuild. To edit content without rebuilding, uncomment the bind mount in docker-compose.yml:

+

A bind mount to a host directory shadows whatever the image's COPY put at that path — including with nothing at all, if the host directory doesn't exist yet or is empty. Docker doesn't seed a bind mount from image content the way it seeds a fresh named volume. Two consequences the plain image can't handle by itself, both worked around by docker-entrypoint.sh (the container's ENTRYPOINT, runs once per start before Apache):

-
volumes:
-  - cache:/var/www/html/public/cache
-  - uploads:/var/www/html/public/uploads
-  - data:/var/www/html/data
-  - ./App:/var/www/html/App
- -

A bind mount at the same path as a COPY'd directory shadows the image layer at container start, so a host-side edit under App/pages/ shows up after docker compose restart web — no rebuild, no custom entrypoint logic.

+
    +
  • Empty App/ on first run. The Dockerfile stashes a pristine copy of the baked-in App/ at /opt/novaconium-app-default at build time. If the bind-mounted /var/www/html/App is empty when the container starts, the entrypoint copies that pristine copy in — so a first docker compose up against a fresh, empty ${VOL_PATH}/novaconium/App on the host produces a working site instead of a blank one. It only seeds when the directory is empty, so it never clobbers content you've already put there.
  • +
  • Permissions. A bind mount keeps the host directory's ownership, not the image's — the build-time chown -R http:http in the Dockerfile only applies to the image layer, not to whatever gets mounted over it. The entrypoint re-runs chown -R http:http on all four mounted paths on every container start, so Apache's worker user (http on Arch) can always write to them regardless of the host-side UID/GID — no manual chmod on the host required.
  • +

MySQL

Lib\Db supports MySQL alongside or instead of SQLite (see Database). docker-compose.yml has a commented-out db service and mysql-data volume — uncomment both, add a db_connections entry with driver: mysql and matching credentials in App/config.php, and the app container can reach it at hostname db.

+

Pinned package versions

+ +

Arch's repos are rolling-release, so the Dockerfile doesn't install apache/php/sqlite from the live mirror — it points pacman at a dated Arch Linux Archive snapshot via the ARCH_SNAPSHOT build arg (a YYYY/MM/DD path), so a rebuild next month installs the exact same versions as today instead of whatever's newest that day. To move to a newer snapshot deliberately (e.g. to pick up a PHP security release):

+ +
docker build --build-arg ARCH_SNAPSHOT=2026/08/01 --no-cache -t novaconium-beta .
+ +

--no-cache is worth using any time you change the Dockerfile itself (not just App/ content) — Docker otherwise reuses a cached layer for an unchanged-looking RUN step, and docker-compose.yml here uses image: rather than build:, so docker compose up alone never rebuilds at all; you have to docker build (and re-tag, if needed) yourself first.

+

Arch-specific notes

-

Arch's php-apache package is built against mpm_prefork, not httpd's default mpm_event — the Dockerfile swaps MPMs as part of the build. The Apache/PHP worker user on Arch is http, not Debian's www-data; the Dockerfile chowns the cache/uploads/data/App directories and novaconium/contact-log.txt to http:http at build time so a freshly created named volume (which inherits the image mountpoint's ownership) is writable immediately. If you swap a named volume for a bind mount pointing at a host directory with different ownership, that automatic chown doesn't apply — you may need to adjust permissions on the host side.

+

Arch's php-apache package is built against mpm_prefork, not httpd's default mpm_event — the Dockerfile swaps MPMs as part of the build. The Apache/PHP worker user on Arch is http, not Debian's www-data; both the build-time chown and docker-entrypoint.sh's startup chown (see above) target http:http for this reason.

This is a separate Dockerfile from the one-off Dart Sass build tool described in Styling — that one lives at Dockerfile.sass, a Debian-based image whose only job is running the sass CLI, not serving the app.

{% endblock %} diff --git a/novaconium/sass/main.sass b/novaconium/sass/main.sass index 3c48089..8617fde 100644 --- a/novaconium/sass/main.sass +++ b/novaconium/sass/main.sass @@ -386,7 +386,7 @@ button opacity: 0 animation: fade-in-up 0.5s ease-out forwards - @for $i from 1 through 6 + @for $i from 1 through 12 &:nth-child(#{$i}) animation-delay: #{0.3 + $i * 0.06}s diff --git a/public/css/main.css b/public/css/main.css index bf7a57d..cca9366 100644 --- a/public/css/main.css +++ b/public/css/main.css @@ -424,6 +424,24 @@ button:hover { .feature-card:nth-child(6) { animation-delay: 0.66s; } +.feature-card:nth-child(7) { + animation-delay: 0.72s; +} +.feature-card:nth-child(8) { + animation-delay: 0.78s; +} +.feature-card:nth-child(9) { + animation-delay: 0.84s; +} +.feature-card:nth-child(10) { + animation-delay: 0.9s; +} +.feature-card:nth-child(11) { + animation-delay: 0.96s; +} +.feature-card:nth-child(12) { + animation-delay: 1.02s; +} .feature-card h2 { font-size: 1.1rem; margin: 0 0 0.5rem;