From 6624c4fdc380ce8ed85a94919accad6e35a46888 Mon Sep 17 00:00:00 2001 From: code Date: Wed, 15 Jul 2026 07:15:17 +0000 Subject: [PATCH] Add Docker support: Arch/Apache/PHP image, three volumes, docs Adds a root Dockerfile (Arch Linux + Apache + PHP) and docker-compose.yml with separate cache/data/images volumes, so a project's own content, page cache, and future uploaded images stay out of paths a framework update would wipe. App/ is baked into the image but can be bind-mounted to override without a rebuild. Renames the Sass build-tool Dockerfile example in the styling doc to Dockerfile.sass to avoid colliding with the new app Dockerfile, adds a new /admin/docs/docker page (linked from the docs index and nav), and documents the reserved images/ directory in AGENTS.md and README. Also records the user's git/docker execution permission boundaries in CLAUDE.md for future sessions. Co-Authored-By: Claude Sonnet 5 --- .dockerignore | 12 +++++ .gitignore | 2 + AGENTS.md | 13 ++++++ CLAUDE.md | 5 +++ Dockerfile | 40 +++++++++++++++++ README.md | 9 ++++ docker-compose.yml | 29 ++++++++++++ images/.gitkeep | 0 .../pages/admin/docs/_layout/layout.twig | 1 + novaconium/pages/admin/docs/docker/index.twig | 45 +++++++++++++++++++ novaconium/pages/admin/docs/index.twig | 1 + .../pages/admin/docs/styling/index.twig | 4 +- 12 files changed, 159 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile create mode 100644 docker-compose.yml create mode 100644 images/.gitkeep create mode 100644 novaconium/pages/admin/docs/docker/index.twig diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e1cb160 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +.git +.gitignore +graphify-out/ +.claude/ +public/cache/* +!public/cache/.gitkeep +data/*.sqlite* +images/* +!images/.gitkeep +docker-compose.yml +Dockerfile +Dockerfile.sass diff --git a/.gitignore b/.gitignore index 20e923f..397ada8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ /data/*.sqlite-shm .claude/ /graphify-out/ +/images/* +!/images/.gitkeep diff --git a/AGENTS.md b/AGENTS.md index 03b3f09..e7faa99 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -246,6 +246,19 @@ index below) before `App/migrations/` (project schema) — see the project adds still defaults to no `migrations_dir` at all unless it sets one; the two-root default is specific to `default`. +A sibling top-level `images/` directory (same gitignore-wholesale-with- +`.gitkeep` treatment as `public/cache/`) exists as **reserved scaffolding +for a future image-upload feature — no config key, no upload code, nothing +reads or writes it yet.** It's deliberately a separate directory from +`data/`, not nested under it, even though both are "project data that +survives a framework update": a project may run MySQL, or no database at +all, so coupling image storage to the SQLite volume would be wrong. See +`novaconium/pages/admin/docs/docker/index.twig` for how the Docker Compose +setup gives it its own volume. Don't add an `images_dir` config key until +an actual feature reads it — an unused key would misrepresent the +framework's state, the same reasoning that keeps every other config key +tied to real consuming code. + `Lib\Session` (`novaconium/lib/Session.php`) is a thin wrapper around native PHP sessions (`session_start()`/`$_SESSION`, not a custom store), all-static and lazy-start like `Lib\Csrf` — nothing calls `session_start()` diff --git a/CLAUDE.md b/CLAUDE.md index 43c994c..a75ef03 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1 +1,6 @@ +# Agent permissions + +- Only run `git` commands with the user's explicit permission for that specific command/action. +- Never run `docker` commands (build, compose up, run, etc.) — leave all Docker execution to the user. + @AGENTS.md diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..56eeaf9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,40 @@ +# 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. +FROM archlinux:base + +RUN pacman -Syu --noconfirm --needed apache php php-apache sqlite \ + && pacman -Scc --noconfirm + +# php-apache on Arch is built against mpm_prefork, not httpd's default +# mpm_event — swap MPMs, enable mod_rewrite, point DocumentRoot at +# public/, and wire in mod_php. +RUN sed -i \ + -e 's/^LoadModule mpm_event_module/#LoadModule mpm_event_module/' \ + -e 's/^#LoadModule mpm_prefork_module/LoadModule mpm_prefork_module/' \ + -e '/^#LoadModule rewrite_module/s/^#//' \ + -e 's#DocumentRoot "/srv/http"#DocumentRoot "/var/www/html/public"#' \ + -e 's###' \ + -e 's/^AllowOverride None/AllowOverride 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 \ + && sed -i \ + -e 's/^;extension=pdo_sqlite/extension=pdo_sqlite/' \ + -e 's/^;extension=pdo_mysql/extension=pdo_mysql/' \ + /etc/php/php.ini + +WORKDIR /var/www/html + +COPY novaconium/ ./novaconium/ +COPY public/ ./public/ +COPY App/ ./App/ + +# Runtime-writable paths not covered by named volumes in docker-compose.yml. +RUN mkdir -p public/cache data images \ + && touch novaconium/contact-log.txt \ + && chown -R http:http public/cache data images App novaconium/contact-log.txt + +EXPOSE 80 + +CMD ["httpd", "-D", "FOREGROUND"] diff --git a/README.md b/README.md index c23d614..b4f3b81 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,14 @@ Visit `http://127.0.0.1:8000/` for the static home page, then click around — ` Point the vhost's document root at `public/`, make sure `mod_rewrite` is enabled and `AllowOverride All` is set for that directory so `public/.htaccess` takes effect, and it just works — no build step required. +### Run it in Docker + +``` +docker compose up --build +``` + +Builds an Arch Linux-based Apache/PHP image and serves the site at `http://localhost:8080/`. Three named volumes (`cache`, `data`, `images`) keep the page cache, SQLite database, and a reserved-for-later images directory out of paths the "Updating the framework" workflow below would wipe; `App/` is baked into the image but can be bind-mounted for edits without a rebuild. See [Docker](http://127.0.0.1:8000/admin/docs/docker) for details. + ### Starting a new project Clone this repo and drop its Git history — no Composer scaffold or installer: @@ -86,6 +94,7 @@ php novaconium/bin/create-static-page.php blog/my-new-post The full framework documentation — routing, sidecars, libraries, database, session, content index, XML sitemap, RSS feeds, layouts, static caching, SEO, Matomo analytics, admin authentication, draft pages, styling, project layout, and third-party notices — lives at `/admin/docs` on any running instance (so it travels with the code, no internet connection needed). Highlights: - [Getting started](http://127.0.0.1:8000/admin/docs/getting-started) +- [Docker](http://127.0.0.1:8000/admin/docs/docker) - [Routing](http://127.0.0.1:8000/admin/docs/routing) - [Sidecars](http://127.0.0.1:8000/admin/docs/sidecars) - [Libraries](http://127.0.0.1:8000/admin/docs/libraries) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..489d351 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,29 @@ +services: + web: + build: . + ports: + - "8080:80" + volumes: + - cache:/var/www/html/public/cache + - data:/var/www/html/data + - images:/var/www/html/images + # Uncomment to override the baked-in App/ with a host copy, no rebuild: + # - ./App:/var/www/html/App + + # Optional — only needed if App/config.php adds a db_connections entry + # with driver: mysql. See /admin/docs/database. + # db: + # image: mysql:8 + # environment: + # MYSQL_DATABASE: novaconium + # MYSQL_USER: novaconium + # MYSQL_PASSWORD: change-me + # MYSQL_ROOT_PASSWORD: change-me + # volumes: + # - mysql-data:/var/lib/mysql + +volumes: + cache: + data: + images: + # mysql-data: diff --git a/images/.gitkeep b/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/novaconium/pages/admin/docs/_layout/layout.twig b/novaconium/pages/admin/docs/_layout/layout.twig index 4834f91..a67cfc7 100644 --- a/novaconium/pages/admin/docs/_layout/layout.twig +++ b/novaconium/pages/admin/docs/_layout/layout.twig @@ -8,6 +8,7 @@
  • {{ icons.book() }}Overview
  • {{ icons.book() }}Getting started
  • +
  • {{ icons.book() }}Docker
  • {{ icons.link() }}Routing
  • {{ icons.book() }}Sidecars
  • {{ icons.email() }}Forms
  • diff --git a/novaconium/pages/admin/docs/docker/index.twig b/novaconium/pages/admin/docs/docker/index.twig new file mode 100644 index 0000000..b552ca3 --- /dev/null +++ b/novaconium/pages/admin/docs/docker/index.twig @@ -0,0 +1,45 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% 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 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 content, cache, and (future) images never live inside a path that gets wiped by the "Updating the framework" workflow.

    + +
    docker compose up --build
    + +

    Visit http://localhost:8080/.

    + +

    The volumes

    + +
      +
    • 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.
    • +
    • 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.
    • +
    • imagesimages/ — a reserved, empty directory for a future image-upload feature. Nothing in the framework reads from or writes to it yet — there's no config key or upload code — it's scaffolding only, kept on its own volume deliberately, since a project might use MySQL or no database at all and shouldn't have image storage coupled to the SQLite volume.
    • +
    + +

    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:

    + +
    volumes:
    +  - cache:/var/www/html/public/cache
    +  - data:/var/www/html/data
    +  - images:/var/www/html/images
    +  - ./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.

    + +

    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.

    + +

    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/data/images/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.

    + +

    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/pages/admin/docs/index.twig b/novaconium/pages/admin/docs/index.twig index cda329a..046ea8b 100644 --- a/novaconium/pages/admin/docs/index.twig +++ b/novaconium/pages/admin/docs/index.twig @@ -28,6 +28,7 @@
    • {{ icons.book() }}Getting started — requirements, running locally, deploying on Apache.
    • +
    • {{ icons.book() }}Docker — the Apache/PHP image, its three volumes, and overriding App/ without a rebuild.
    • {{ icons.link() }}Routing — how a URL maps to a directory under App/pages/.
    • {{ icons.book() }}Sidecars — where your PHP logic goes.
    • {{ icons.email() }}Forms — building a custom form with a sidecar, using the novaconium validation/spam libraries.
    • diff --git a/novaconium/pages/admin/docs/styling/index.twig b/novaconium/pages/admin/docs/styling/index.twig index f98d2f9..6c996d7 100644 --- a/novaconium/pages/admin/docs/styling/index.twig +++ b/novaconium/pages/admin/docs/styling/index.twig @@ -15,7 +15,7 @@

      There's no PHP-based Sass compiler in this project (PHP options like scssphp only understand SCSS syntax) — this is a manual/CI build step, not something the app does at runtime.

      -

      Don't have Dart Sass installed? Run it via Docker instead — copy-paste this Dockerfile, which installs the same official standalone Dart Sass release used in this environment (1.101.0, via pacman -S dart-sass on Arch), not the npm-wrapped build:

      +

      Don't have Dart Sass installed? Run it via Docker instead — copy-paste this into a file named Dockerfile.sass (the project's own root Dockerfile is the app container, see Docker — this is a separate, one-off build tool, so it gets its own filename), which installs the same official standalone Dart Sass release used in this environment (1.101.0, via pacman -S dart-sass on Arch), not the npm-wrapped build:

      FROM debian:bookworm-slim
       
      @@ -38,7 +38,7 @@ ENTRYPOINT ["sass"]

      Build it once, then run it the same way you'd run the local sass CLI (skip the leading sass in the command — the image's ENTRYPOINT already supplies it):

      -
      docker build -t novaconium-sass -f Dockerfile .
      +    
      docker build -t novaconium-sass -f Dockerfile.sass .
       docker run --rm -v "$(pwd):/usr/src/app" -w /usr/src/app novaconium-sass \
           --load-path=App/sass --load-path=novaconium/sass/defaults novaconium/sass/main.sass public/css/main.css