diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..e83d101 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,395 @@ +# casjaysdevdocker repo template spec + +This file is the **canonical spec** for what a properly-set-up `casjaysdevdocker/` container repo looks like. It is the source of truth for migrations, audits, and new repos. + +- Untracked: this file lives at the repos root, not in any individual repo's git tree. +- Per-repo: each repo gets `/CLAUDE.md` (a copy of this file) and `/PLAN.md` (the concrete plan for that specific service stack), both committed to the repo. +- Authority: `~/.claude/CLAUDE.md` (52 numbered global rules) is the base. This file extends it for the docker-image work. Existing repo files are *input*, never authoritative — the repos are mostly unfinished. + +--- + +## 1. What a repo is + +Each repo builds **one self-contained Docker image** for the service named after the repo. The image: + +- Targets `linux/amd64` and `linux/arm64` via `buildx`. +- Boots via `tini` → `entrypoint.sh` → init.d service scripts → long-running service. +- Stores user-modifiable config under `/config/` (volume). +- Stores runtime data, logs, DB files under `/data` (volume). +- Ships a single, **highly-optimized** config in `rootfs/tmp/etc//` that replaces the distro defaults at build time. +- Provides sane defaults: first-run with no env vars works for the common case. + +App-style repos (ampache, wordpress, navidrome, …) ship the **whole stack** the app needs: the app itself at `/usr/local/share/`, a webserver (apache or nginx) to serve it, php-fpm if PHP-based, and a database (mariadb/postgres) if the app needs one. DB-server repos (mariadb, postgres, mongodb, …) ship the DB **plus** apache + php-fpm + the canonical web admin UI (phpmyadmin for mariadb/mysql; pgAdmin/phpPgAdmin for postgres; mongo-express for mongodb; etc.). + +Inferred intent from repo name examples: + +| Repo | What it is | +|-----------------|-------------------------------------------------------------------------------------------------------------------------| +| `nginx` | nginx webserver; PHP-FPM upstream on `:9000`; CGI/Lua/etc via `nginx-mod-*` packages | +| `apache`/`httpd`| apache2 webserver + php-fpm; mod_rewrite/proxy/ssl/etc | +| `cherokee` | cherokee webserver with **CGI handlers for Ruby, Perl, Python** + **full PHP support** | +| `lighttpd` | lighttpd webserver + php-fpm/cgi | +| `caddy` | Caddy webserver (reverse-proxy/auto-TLS) | +| `traefik` | Traefik edge router | +| `mariadb` | mariadb-server + apache + php-fpm + **phpmyadmin** at `/usr/local/share/phpmyadmin` | +| `mysql` | mysql + apache + php-fpm + phpmyadmin | +| `postgres` | postgresql + apache + php-fpm + **phpPgAdmin/pgAdmin** at `/usr/local/share/phppgadmin` | +| `mongodb` | mongodb + apache + php-fpm + mongo-express or comparable web UI | +| `couchdb` | couchdb (built-in Fauxton UI) | +| `redis`/`valkey`| key-value store; CLI tools | +| `ampache` | apache + php-fpm + mariadb + ampache app at `/usr/local/share/ampache`; serves the music UI on `:80` | +| `wordpress` | apache + php-fpm + mariadb + wordpress at `/usr/local/share/wordpress` | +| `nextcloud` | apache + php-fpm + mariadb + nextcloud at `/usr/local/share/nextcloud` | +| `gitea`/`forgejo`| the binary, sqlite by default | +| `bind` | bind9 DNS server | +| `tor` | tor daemon | +| `ssl-ca` | a self-signed CA + `openssl`/`certbot` tooling | + +**Always cross-check intent and packages against:** +- **Distro docs for file paths**: Alpine docs for `/etc/` layout (and `pkgs.alpinelinux.org` for available package names); Alma/Rocky docs for `/etc/httpd` (rhel-family path). +- **Project docs for config content**: `nginx.org` for `nginx.conf` directives, `httpd.apache.org` for `httpd.conf`, `mariadb.com` for `my.cnf`, etc. +- **Upstream Docker images**: when available, `docker pull /:latest` and inspect (`docker history`, `docker run --rm -it ... sh`) to confirm canonical install paths and config locations. **Always `docker rmi` to clean up after.** + +Never invent a package name or a config option. `WebFetch`/`WebSearch`/`docker pull` to verify. + +--- + +## 2. File inventory (every repo) + +``` +/ +├── CLAUDE.md # copy of TEMPLATE.md, committed; agent reads this when working in this repo +├── PLAN.md # this repo's concrete plan: packages, configs, init.d behavior, success criteria +├── README.md # user-facing install/run docs +├── LICENSE.md # MIT (per global rule 32) or per-repo as appropriate +├── Dockerfile # multi-stage, alpine-based by default (rhel for systemd-only repos) +├── .env.scripts # buildx wrapper config (registry, push, pull, packages list) +├── .dockerignore +├── .gitattributes +├── .gitignore +├── .gitea/workflows/docker.yaml # gitea CI workflow +├── Jenkinsfile # optional, if the repo had one +└── rootfs/ # everything under here is COPYed to / in the build + ├── usr/local/bin/ + │ ├── entrypoint.sh # SHARED template, only the description + CONTAINER_NAME line are repo-specific + │ └── pkmgr # SHARED template (apt/dnf/apk auto-detect wrapper) + ├── usr/local/etc/docker/ + │ ├── functions/entrypoint.sh # framework functions sourced by entrypoint.sh and 99-.sh + │ └── init.d/ + │ └── 99-.sh # PER-REPO; defines SERVICE_NAME, ETC_DIR, CONF_DIR, EXEC_CMD_BIN/ARGS, hooks + ├── usr/local/share/ + │ ├── / # for app-style repos: the actual app code (e.g., ampache, wordpress) + │ ├── phpmyadmin/ # for mariadb/mysql repos + │ ├── phppgadmin/ # for postgres repos + │ ├── httpd/default/ # default webroot (used when no app) + │ └── template-files/ + │ ├── config/ # templates copied to /config// on first run by __initialize_config_dir + │ ├── data/ # templates copied to /data// on first run + │ └── defaults/ # default fallbacks + ├── tmp/etc// # PER-REPO optimized configs; copied to /etc// at build time (see §4) + ├── tmp/bin/ # optional; auto-installed to /usr/local/bin/ at build time + ├── tmp/var/ # optional; auto-installed to /var/ at build time + └── root/docker/setup/ # PER-REPO build-time scripts (see §3) + ├── 00-init.sh + ├── 01-system.sh + ├── 02-packages.sh + ├── 03-files.sh + ├── 04-users.sh + ├── 05-custom.sh # ← service-specific config wipe-and-replace lives here + ├── 06-post.sh + └── 07-cleanup.sh +``` + +**SHARED vs PER-REPO** (load-bearing distinction — getting this wrong breaks repos): + +- **SHARED** (safe to overwrite from the upstream template): `rootfs/usr/local/bin/entrypoint.sh` (only the description + `CONTAINER_NAME=""` change per repo), `rootfs/usr/local/bin/pkmgr`, `rootfs/usr/local/etc/docker/functions/entrypoint.sh`. +- **PER-REPO** (never overwrite from a template — read first; preserve service-specific logic): every script under `rootfs/root/docker/setup/`, `rootfs/usr/local/etc/docker/init.d/*`, everything under `rootfs/tmp/`, everything under `rootfs/usr/local/share//`. + +--- + +## 3. Build-time setup script flow + +`Dockerfile` runs `00-init.sh` → … → `07-cleanup.sh` interleaved with package install. Order and contract: + +| Script | When it runs | What it does | +|------------------|-----------------------------------------------|----------------------------------------------------------------------------------------------------| +| `00-init.sh` | Right after `mkdir`s before anything else | Sanity setup; usually empty | +| `01-system.sh` | After apk repos are configured | System-level tweaks (extra repos, timezone, non-package OS config) | +| `02-packages.sh` | After `pkmgr install $PACK_LIST` | Per-service post-install tweaks (e.g., compile a module, install a pip/npm package, fetch the app) | +| `03-files.sh` | After packages are installed | **Auto-installs `rootfs/tmp/{bin,var,etc,data}/*`** into `/usr/local/bin/`, `/var/`, `/etc/`, and the `template-files/` staging dirs. Most repos use the canonical version verbatim. | +| `04-users.sh` | After files are placed | Create system users/groups the service needs (e.g., `nginx`, `mysql`, `apache`) | +| `05-custom.sh` | After users | **Service-specific config wipe-and-replace** (see §4). Also: clone wwwroot templates, fetch app source, etc. | +| `06-post.sh` | After custom | Late tweaks (permissions, symlinks) | +| `07-cleanup.sh` | Last | Per-service cache cleanup beyond the Dockerfile's generic cleanup | + +The Dockerfile's own RUN steps already do generic cleanup (`pkmgr clean`, `rm -Rf /usr/share/doc/*`, etc.); the per-script `07-cleanup.sh` only handles service-specific files. + +--- + +## 4. The wipe-and-replace config flow + +The most important pattern in this template. Goal: the running container's `/etc//` contains **only** our optimized config, never distro defaults. + +**Build time:** +1. Service package install (e.g., `apk add nginx`) creates distro defaults under `/etc//` (e.g., `/etc/nginx/{nginx.conf, conf.d/, modules-enabled/, http.d/, mime.types, …}`). +2. `03-files.sh` copies `rootfs/tmp/etc//*` → `/etc//*` (overlay only) **and** stages a copy at `/usr/local/share/template-files/config//` for runtime seeding. +3. `05-custom.sh` performs the **wipe-and-replace** (the canonical idiom): + ```sh + if [ -d "/tmp/etc/" ]; then + # preserve distro-shipped files we need (e.g., mime.types when not in tmp/etc/) + # then wipe defaults + rm -Rf "/etc/"/* + cp -Rf "/tmp/etc//." "/etc//" + fi + ``` + For services that auto-discover sub-confs, our `.conf` ends with an **optional** include like `include /config//vhosts.d/*.conf;` (nginx) or `IncludeOptional /config//conf.d/*.conf` (apache) so an empty include dir doesn't crash startup. + +**Runtime (entrypoint + 99-.sh):** +1. `entrypoint.sh` calls `__initialize_default_templates` / `__initialize_config_dir` / `__initialize_data_dir` which copy `template-files/{defaults,config,data}//*` → `/config//` and `/data//` **only when those target dirs are not already initialized** (via `/config/.docker_has_run` and `/data/.docker_has_run` markers). +2. `99-.sh` calls `__initialize_system_etc "$CONF_DIR"` which symlinks/copies the user-editable `/config//` → the service's expected runtime path (`/etc//` for system services; `/usr/local/share//config/` for app-stack repos). +3. `99-.sh` also ensures runtime dirs exist: `vhosts.d/`, `conf.d/`, `ssl/`, `secure/auth/`, log dirs under `/data/logs//`. +4. Service starts pointing at `/etc//.conf` (or equivalent), which transitively reads from `/config//`. + +Net effect: end users edit files under `/config//` (volume); the service picks them up; rebuilds and restarts don't trample user changes. + +**Anti-patterns:** +- Letting distro defaults survive into the running image (didn't wipe `/etc//*`). +- Hardcoding paths in our config that point inside `/etc//` instead of `/config//` for things users should customize. +- Copying `template-files/config//*` into `/config//` unconditionally on every container start (clobbers user edits) — always gate with the init markers. +- Using a non-optional `include` for `vhosts.d/` (kills the service when the dir is empty on first run). + +--- + +## 5. Dockerfile structure + +Multi-stage. Build stage installs packages and runs setup scripts; final stage is `FROM scratch` and `COPY --from=build /. /` for a minimal final image. Exception: containers needing systemd as PID 1 (e.g., blueonyx) are single-stage with `CMD ["/sbin/init"]`. + +Required ARGs in the **header** (preserve per-repo values during migration): + +```dockerfile +ARG IMAGE_NAME="" +ARG PHP_SERVER="" # often same as IMAGE_NAME +ARG BUILD_DATE="" # auto-bumped by gen-dockerfile / CI +ARG LANGUAGE="en_US.UTF-8" +ARG TIMEZONE="America/New_York" +ARG WWW_ROOT_DIR="/usr/local/share/httpd/default" +ARG DEFAULT_FILE_DIR="/usr/local/share/template-files" +ARG DEFAULT_DATA_DIR="/usr/local/share/template-files/data" +ARG DEFAULT_CONF_DIR="/usr/local/share/template-files/config" +ARG DEFAULT_TEMPLATE_DIR="/usr/local/share/template-files/defaults" +ARG PATH="/usr/local/etc/docker/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" +ARG USER="root" +ARG SHELL_OPTS="set -e -o pipefail" +ARG SERVICE_PORT="" +ARG EXPOSE_PORTS="" +ARG PHP_VERSION="" +ARG NODE_VERSION="system" +ARG NODE_MANAGER="system" +ARG IMAGE_REPO="/" +ARG IMAGE_VERSION="latest" +ARG CONTAINER_VERSION="" # "USE_DATE" if buildx should auto-add a date tag +ARG PULL_URL="casjaysdev/alpine" # or "alpine", "almalinux/10-init", etc. +ARG DISTRO_VERSION="${IMAGE_VERSION}" +ARG BUILD_VERSION="${BUILD_DATE}" +``` + +`PACK_LIST` lives in the build stage and is the **single most repo-specific value** — it must be complete and accurate for the service stack: + +```dockerfile +ARG PACK_LIST="" +``` + +Build-stage RUN order (alpine): + +1. `COPY ./rootfs/. /` (early, so setup scripts and `/tmp/etc/` are available before package install) +2. `RUN pkmgr update; pkmgr install bash` +3. `RUN` install bash + symlink `/bin/sh` → `bash` (Alpine ships busybox sh) +4. `ENV SHELL="/bin/bash"; SHELL [ "/bin/bash", "-c" ]` +5. `COPY --from=gosu /usr/local/bin/gosu /usr/local/bin/gosu` +6. `RUN` initialize: `mkdir -p` template dirs + `00-init.sh` +7. `RUN` system: rewrite `/etc/apk/repositories` for the right distro version; `apk update && apk upgrade` + `01-system.sh` +8. `RUN` pre-package commands (usually empty) +9. `RUN` install packages from `${PACK_LIST}` via `pkmgr install` +10. `RUN` `02-packages.sh` +11. `COPY ./Dockerfile /root/docker/Dockerfile` (so the image carries its own build recipe) +12. `RUN` updating system files: timezone, nsswitch, php symlinks, .bashrc, `03-files.sh` +13. `RUN` Custom Settings (usually empty placeholder) +14. `RUN` users + `04-users.sh` +15. `RUN` user init (placeholder) +16. `RUN` OS Settings (placeholder) +17. `RUN` Custom Applications (placeholder) +18. `RUN` `05-custom.sh` +19. `RUN` final commands + `06-post.sh` +20. `RUN` cleanup (generic) + `07-cleanup.sh` +21. `RUN echo "Init done"` + +Final stage (`FROM scratch`): + +- ARGs re-declared (scratch needs them); ENVs set; standard LABEL block (URLs, vendor, revision = `${GIT_COMMIT}`); `COPY --from=build /. /`; `VOLUME [ "/config","/data" ]`; `EXPOSE ${SERVICE_PORT} ${ENV_PORTS}`; `STOPSIGNAL SIGRTMIN+3`; `ENTRYPOINT [ "tini", "-p", "SIGTERM", "--", "/usr/local/bin/entrypoint.sh" ]`; `HEALTHCHECK ... CMD [ "/usr/local/bin/entrypoint.sh", "healthcheck" ]`. + +Distro variants: +- **alpine** (default for ~all repos) +- **rhel/almalinux** (for repos that need RHEL packages or systemd) — generated via `gen-dockerfile --dir rhel`. Almost all are still single-stage with `CMD ["/sbin/init"]`. + +Generic Dockerfile bug fixes to apply when migrating any repo (these are upstream gen-dockerfile bugs): +- Missing space before `]`: `[ "$SH_CMD" != "/bin/sh"]` → `[ "$SH_CMD" != "/bin/sh" ]` +- Blank line inside the "Creating and editing system files" RUN block (after `$SHELL_OPTS; \`) — remove the blank line; line continuation is broken otherwise. +- Unindented `echo ""` in the "Custom Settings" and "Custom Applications" RUN blocks — re-indent to ` echo ""`. + +--- + +## 6. .env.scripts fields + +The buildx wrapper (`/usr/local/bin/buildx`) reads `.env.scripts`. Required fields (current names — older field names are deprecated): + +```sh +ENV_DOCKERFILE="Dockerfile" +ENV_REGISTRY_REPO="" # was ENV_IMAGE_NAME +ENV_USE_TEMPLATE="alpine" # or "almalinux", "debian", "ubuntu" +ENV_REGISTRY_ORG="" # was ENV_ORG_NAME; must match the org in ENV_REGISTRY_PUSH +ENV_VENDOR="CasjaysDev" +ENV_AUTHOR="CasjaysDev" +ENV_MAINTAINER="CasjaysDev " +ENV_GIT_REPO_URL="https://github.com//" +ENV_REGISTRY_URL="https://docker.io" # full URL, not bare "docker.io" +ENV_REGISTRY_PUSH="/" # was ENV_IMAGE_PUSH +ENV_IMAGE_TAG="latest" +ENV_ADD_TAGS="" # "USE_DATE" to auto-add YYMM tag +ENV_ADD_IMAGE_PUSH="" +ENV_PULL_URL="" # e.g. "casjaysdev/alpine", "alpine", "almalinux/10-init" +ENV_DISTRO_TAG="${IMAGE_VERSION}" +ENV_PLATFORMS="linux/amd64,linux/arm64" # only emit when overriding the default both-archs +SERVICE_PORT="" +EXPOSE_PORTS="" +LANG_VERSION="" +PHP_VERSION="" +NODE_VERSION="system" +NODE_MANAGER="system" +WWW_ROOT_DIR="/usr/local/share/httpd/default" +DEFAULT_FILE_DIR="/usr/local/share/template-files" +DEFAULT_DATA_DIR="/usr/local/share/template-files/data" +DEFAULT_CONF_DIR="/usr/local/share/template-files/config" +DEFAULT_TEMPLATE_DIR="/usr/local/share/template-files/defaults" +ENV_PACKAGES="" +``` + +`ENV_PACKAGES` and Dockerfile `PACK_LIST` must stay in sync. Single-space separation, no double spaces. + +--- + +## 7. init.d/99-.sh contract + +Each repo's primary init.d script (named `99-.sh` for late ordering, or `09-.sh`/etc. when ordering matters relative to other init.d entries — e.g., php-fpm starts before nginx) defines repo-specific state and calls framework functions defined in `rootfs/usr/local/etc/docker/functions/entrypoint.sh`. Required variable assignments at the top (use the nginx 99-nginx.sh as the reference structure): + +```sh +SERVICE_NAME="" +DATA_DIR="/data/" +CONF_DIR="/config/" +ETC_DIR="/etc/" # or /usr/local/share//config for app-stack repos +TMP_DIR="/tmp/" +RUN_DIR="/run/" +LOG_DIR="/data/logs/" +SERVICE_PORT="" +SERVICE_USER="" # the daemon's run-as user +SERVICE_GROUP="" +EXEC_CMD_BIN='' # e.g., 'nginx', 'mysqld', 'httpd' +EXEC_CMD_ARGS='' # e.g., '-c $ETC_DIR/nginx.conf' +IS_WEB_SERVER="yes|no" +IS_DATABASE_SERVICE="yes|no" +USES_DATABASE_SERVICE="yes|no" +DATABASE_SERVICE_TYPE="" +ADDITIONAL_CONFIG_DIRS="" # extra /config subdirs to seed/symlink +APPLICATION_FILES="..." +APPLICATION_DIRS="$ETC_DIR $CONF_DIR $LOG_DIR $TMP_DIR $RUN_DIR $VAR_DIR" +``` + +Repo-customizable hooks (override the `_local` variants — the framework calls them at the right time): + +- `__run_precopy_local` — before any /config copy +- `__execute_prerun_local` — pre-execution setup +- `__run_pre_execute_checks_local` — final preflight checks +- `__update_conf_files_local` — token replacement in `/etc//*` (use `__replace`/`__find_replace`) +- `__pre_execute_local` — last-mile actions +- `__post_execute_local` — actions in background after service start +- `__pre_message_local` — pre-launch banner +- `__update_ssl_conf_local` — repo-specific SSL handling + +--- + +## 8. Per-repo CLAUDE.md and PLAN.md + +When working on a repo, the agent should: + +1. `cd ` +2. Read `CLAUDE.md` (this file's content, copied) for the spec. +3. Read `PLAN.md` for repo-specific decisions. +4. Read every existing file in the repo before editing it (rule 8). +5. When uncertain about a package/path/option, `WebFetch` the relevant docs (distro for paths, project for content). Never invent. + +`PLAN.md` template (commit this in each repo): + +```markdown +# migration plan + +## Service intent + + +## Service stack +- : ; canonical config at +- : ... + +## Packages (PACK_LIST / ENV_PACKAGES) + + +## Configs to ship in rootfs/tmp/etc/ +- /etc//: ; +- ... + +## /config// layout (user-editable) +- -> symlinked to +- vhosts.d/ -> include /config//vhosts.d/*.conf (optional) + +## init.d/99-.sh +- SERVICE_NAME, EXEC_CMD_BIN, EXEC_CMD_ARGS +- IS_WEB_SERVER / IS_DATABASE_SERVICE / USES_DATABASE_SERVICE +- Repo-specific hooks needed: __update_conf_files_local (replace XYZ), ... + +## 05-custom.sh additions +- Wipe /etc//* and copy from /tmp/etc//. +- Fetch if not present +- Other service-specific install steps + +## Verification (success criteria) +- buildx run Dockerfile succeeds for linux/amd64 + linux/arm64 +- docker run -d -p : ... starts cleanly; logs show no errors +- curl -fsS http://localhost:/ returns the expected response +- /config// is seeded on first run; editing a file there changes service behavior on restart +- (DB repos) connecting with the right CLI client succeeds +- (optional) compared against upstream image (`docker pull :latest && docker history :latest`); upstream image deleted (`docker rmi`) after verification +``` + +--- + +## 9. Migration workflow per repo + +1. Create `/CLAUDE.md` = copy of this `TEMPLATE.md`. +2. Read existing files; assemble the PLAN.md. +3. **Read each file before changing it** (no batch templating). Apply only the changes that PLAN.md identifies. +4. `cd && rm -f .build_failed && buildx run Dockerfile` — fix any build error before moving on. +5. Smoke-test: `docker run --rm -d --name test- -p : /:latest`; wait for healthcheck; `curl` the health/main endpoint; `docker exec` and inspect `/config//`; stop and remove. +6. Commit `CLAUDE.md` and `PLAN.md` to the repo (do NOT commit code changes unless the user has asked — global rule about commits applies). +7. Move to next repo with **no carry-over**: spawn a fresh subagent; do not load prior repo's context. + +--- + +## 10. Anti-patterns (never do) + +- Overwriting `rootfs/root/docker/setup/*.sh` from a generic template — these are per-repo. +- Overwriting `rootfs/usr/local/etc/docker/init.d/*.sh` from a template — per-repo. +- Overwriting `rootfs/tmp/etc//*` — per-repo. +- Overwriting `rootfs/usr/local/share//*` — per-repo (the actual application code). +- Inventing package names, config keys, or service paths — verify with distro/project docs. +- Hardcoding secrets, tokens, internal hostnames (rule 39: every repo is public). +- Skipping the wipe-and-replace step (leaves distro defaults active alongside our config). +- Using a non-optional include for vhosts.d / conf.d (empty dir crashes the service). +- Calling a repo "done" because `buildx` was green; "done" requires the smoke-test passing. diff --git a/PLAN.md b/PLAN.md new file mode 100644 index 0000000..0480e46 --- /dev/null +++ b/PLAN.md @@ -0,0 +1,163 @@ +# ampache migration plan + +## Service intent + +Self-hosted music streaming server. Single Alpine-based Docker image bundling Apache (httpd) + PHP-FPM (php84) + MariaDB-server + the Ampache web app at `/usr/local/share/ampache`. Apache serves the app's `public/` subfolder (the canonical Ampache web root since v5+). Container exposes `:80`; first run takes the user to `install.php` which provisions the database and creates an admin account. Volumes: `/config` (user-editable settings: ampache config, httpd conf snippets, php-fpm pool, my.cnf, secure/auth) and `/data` (mariadb datadir, logs, uploaded media library if mounted there). + +## Service stack + +- Web server: `apache2` (Alpine) -> `/usr/sbin/httpd`, main config `/etc/apache2/httpd.conf`, vhost include in `/etc/apache2/conf.d/*.conf`. Uses event MPM, mod_rewrite + AllowOverride All for the Ampache `.htaccess`. +- App runtime: `php84-fpm` (Alpine) -> `/usr/sbin/php-fpm84`, conf `/etc/php84/php-fpm.conf` + pool `/etc/php84/php-fpm.d/www.conf`. Apache talks to it via `proxy_fcgi` over a unix socket `/run/php-fpm/php-fpm.sock` (no exposed TCP). +- Database: `mariadb` + `mariadb-client` (Alpine) -> `/usr/bin/mariadbd`, datadir `/data/db/mariadb`, socket `/run/mysqld/mysqld.sock`. Started by a separate `09-mariadb.sh` so it's up before ampache is rendered. +- Application: Ampache 7.9.3 (`ampache-7.9.3_all_php8.4.zip` from upstream GitHub releases — pre-built, vendor/ + node_modules/ already populated, no composer/npm needed at build time). Installed at `/usr/local/share/ampache`; Apache `DocumentRoot` is `/usr/local/share/ampache/public`. + +## Packages (PACK_LIST / ENV_PACKAGES) + +Trimmed from the original kitchen-sink list in the existing Dockerfile. Each package is on `pkgs.alpinelinux.org` (verified for the `edge` branch). + +System glue: +- `bash`, `tini`, `curl`, `wget`, `unzip`, `tzdata`, `ca-certificates`, `pwgen` — entrypoint, fetching/extracting the ampache zip, password generation. +- `tar`, `gzip` — archive handling. + +Apache: +- `apache2`, `apache2-ctl`, `apache2-utils` — server binary, control script, htpasswd/etc. +- `apache2-ssl` — mod_ssl (TLS support; off by default but available). +- `apache2-proxy` — mod_proxy + mod_proxy_fcgi (required to forward PHP requests to php-fpm). +- `apache2-http2` — http/2 support. +- `apache2-brotli` — mod_brotli compression. +- `apache2-icons`, `apache2-error` — icons + multilingual error pages. + +(Dropped from prior list: `apache2-lua` `apache2-ldap` `apache2-webdav` `apache2-mod-wsgi` `apache-mod-fcgid` `apache2-proxy-html` — none are needed for ampache and `apache2-proxy` is the canonical fcgi backend, not `mod_fcgid`.) + +PHP 8.4 — only the modules Ampache actually requires per upstream docs (PDO, PDO_MYSQL, hash, session, intl, json, curl, simplexml + optional gd, ldap, zip), plus FPM and the bare runtime: +- `php84` `php84-fpm` `php84-common` `php84-ctype` `php84-pdo` `php84-pdo_mysql` `php84-mysqli` `php84-mysqlnd` `php84-session` `php84-intl` `php84-curl` `php84-simplexml` `php84-xml` `php84-xmlreader` `php84-xmlwriter` `php84-dom` `php84-mbstring` `php84-iconv` `php84-tokenizer` `php84-fileinfo` `php84-openssl` `php84-phar` `php84-gd` `php84-zip` `php84-bz2` `php84-gmp` `php84-exif` `php84-opcache` `php84-pecl-redis` + +(Dropped from prior list: dba, dev, doc, embed, enchant, ffi, ftp, gettext, imap, ldap (kept off — not needed by ampache out-of-box), litespeed, odbc, pcntl, pdo_dblib, pdo_odbc, pdo_pgsql, pdo_sqlite, pear, pgsql, phpdbg, posix, pspell, shmop, snmp, soap, sockets, sodium, sqlite3, sysvmsg, sysvsem, sysvshm, tidy, xsl, pecl-memcached, pecl-mcrypt, pecl-mongodb, calendar, cgi, bcmath. Kept what either a stock Ampache install touches or is part of ampache's bundled vendor extensions. `composer` is dropped — we use the prebuilt zip, no install step needed.) + +MariaDB: +- `mariadb` — server (`/usr/bin/mariadbd`). +- `mariadb-client` — `mariadb` CLI client used by the post-execute initdb step. +- `mariadb-server-utils` — `mysql_install_db`, `mariadb-admin`, `mariadb-secure-installation`. + +## Configs to ship in rootfs/tmp/etc/ + +Wipe-and-replace at build time (per template §4). All paths under `rootfs/tmp/etc/`. + +- `apache2/httpd.conf` — minimal Alpine apache main config: load only the modules we need (mpm_event, mime, dir, alias, authz_core, authz_host, autoindex, deflate, brotli, expires, headers, log_config, mime_magic, negotiation, proxy, proxy_fcgi, rewrite, setenvif, ssl, status, unixd, http2). User/Group `apache:apache`. ServerRoot `/var/www`. Logs to `/data/logs/apache2/{access,error}.log`. PidFile `/run/apache2/httpd.pid`. ErrorLog/LogLevel sensible defaults. Final line: `IncludeOptional /etc/apache2/conf.d/*.conf` and `IncludeOptional /config/apache2/vhosts.d/*.conf` (optional, so empty dir doesn't crash). +- `apache2/conf.d/ampache.conf` — vhost: `` with `DocumentRoot /usr/local/share/ampache/public`, `` block (`Options FollowSymLinks`, `AllowOverride All`, `Require all granted`), `ProxyPassMatch ^/(.+\.php(/.*)?)$ unix:/run/php-fpm/php-fpm.sock|fcgi://localhost/usr/local/share/ampache/public/$1`, `DirectoryIndex index.php index.html`. Sets `SetEnvIf Authorization "(.*)" HTTP_AUTHORIZATION=$1` (per upstream Apache vhost, needed for ampache API auth headers). ErrorLog/CustomLog under `/data/logs/apache2/`. +- `apache2/conf.d/mpm.conf` — switch to mpm_event explicitly + sane worker tunings. +- `php84/php.ini` — production-tuned: `memory_limit = 512M`, `upload_max_filesize = 256M`, `post_max_size = 256M`, `max_execution_time = 300`, `date.timezone = ${TZ}`, `expose_php = Off`, `cgi.fix_pathinfo = 0`, `opcache.enable = 1`, `opcache.memory_consumption = 256`, `opcache.max_accelerated_files = 20000`, `session.save_path = /tmp/php-sessions`. +- `php84/php-fpm.conf` — global: `pid = /run/php-fpm/php-fpm.pid`, `error_log = /data/logs/php-fpm/error.log`, `daemonize = no` (we run under our own supervisor), `include=/etc/php84/php-fpm.d/*.conf`. +- `php84/php-fpm.d/www.conf` — pool `[www]`, `user = apache`, `group = apache`, `listen = /run/php-fpm/php-fpm.sock`, `listen.owner = apache`, `listen.group = apache`, `listen.mode = 0660`, `pm = dynamic`, `pm.max_children = 20`, `pm.start_servers = 4`, `pm.min_spare_servers = 2`, `pm.max_spare_servers = 8`, `pm.max_requests = 500`, `clear_env = no`. +- `my.cnf.d/mariadb-server.cnf` — server config: `[mysqld]` with `datadir = /data/db/mariadb`, `socket = /run/mysqld/mysqld.sock`, `bind-address = 127.0.0.1` (DB only reachable inside container), `port = 3306`, `character-set-server = utf8mb4`, `collation-server = utf8mb4_unicode_ci`, `max_allowed_packet = 64M`, `innodb_buffer_pool_size = 256M`, `log_error = /data/logs/mariadb/mariadb.err.log`, `pid-file = /run/mysqld/mariadb.pid`. `[client]` `socket = /run/mysqld/mysqld.sock`. `[mysqld_safe]` matching log path. +- `ampache/ampache.cfg.php.dist` — empty placeholder (or a copy of the ampache-shipped dist file). Real `ampache.cfg.php` is generated by `install.php` on first web visit. + +## /config// layout (user-editable) + +The framework's `__initialize_system_etc` symlinks every file under `/config//` back to its `/etc//` peer. So our `/config/` seed (via `template-files/config/`) mirrors `/etc/` with the same paths: + +- `/config/apache2/httpd.conf` -> symlinked to `/etc/apache2/httpd.conf` +- `/config/apache2/conf.d/ampache.conf` -> symlinked to `/etc/apache2/conf.d/ampache.conf` +- `/config/apache2/conf.d/mpm.conf` -> symlinked to `/etc/apache2/conf.d/mpm.conf` +- `/config/apache2/vhosts.d/*.conf` -> picked up by the `IncludeOptional` line for user-supplied vhosts +- `/config/php84/php.ini` -> `/etc/php84/php.ini` +- `/config/php84/php-fpm.conf` -> `/etc/php84/php-fpm.conf` +- `/config/php84/php-fpm.d/www.conf` -> `/etc/php84/php-fpm.d/www.conf` +- `/config/my.cnf.d/mariadb-server.cnf` -> `/etc/my.cnf.d/mariadb-server.cnf` +- `/config/ampache/ampache.cfg.php` -> bind-mounted into `/usr/local/share/ampache/config/ampache.cfg.php` (created post-install by `install.php`; we symlink the live file out to `/config/ampache/` after install completes so it survives container recreation). +- `/config/secure/auth/{root,user}/{ampache,mariadb}_{name,pass}` -> generated/used by the framework +- `/config/env/{ampache,mariadb}.sh` -> per-service env overrides + +ADDITIONAL_CONFIG_DIRS for ampache will be `/config/apache2 /config/php84 /config/my.cnf.d /config/ampache` so each one runs through `__initialize_system_etc`. + +## init.d/99-ampache.sh (and 09-mariadb.sh) + +Two init.d scripts. MariaDB starts first (`09-`), Apache+PHP-FPM start under ampache (`99-`). + +`rootfs/usr/local/etc/docker/init.d/09-mariadb.sh` — copy of mariadb repo's `09-mariadb.sh` with these knobs: +- `SERVICE_NAME="mariadb"`, `EXEC_CMD_BIN='mariadbd'`, `EXEC_CMD_ARGS='--user=$SERVICE_USER --datadir=$DATABASE_DIR --socket=/run/mysqld/mysqld.sock'` +- `SERVICE_USER="mysql"`, `SERVICE_GROUP="mysql"` +- `IS_DATABASE_SERVICE="yes"`, `DATABASE_SERVICE_TYPE="mariadb"` +- `__run_pre_execute_checks_local`: bootstrap datadir if missing (`mysql_install_db --datadir=$DATABASE_DIR --user=mysql`). +- `__post_execute_local`: when first run, create the `ampache` database + `ampache` user + grant; write password to `/config/secure/auth/user/ampache_db_pass`. + +`rootfs/usr/local/etc/docker/init.d/99-ampache.sh` — based on nginx's 99-nginx.sh structure: +- `SERVICE_NAME="ampache"`, `SERVICE_USER="apache"`, `SERVICE_GROUP="apache"` +- `EXEC_CMD_BIN='httpd'`, `EXEC_CMD_ARGS='-D FOREGROUND -f /etc/apache2/httpd.conf'` +- `EXEC_PRE_SCRIPT='/usr/sbin/php-fpm84'` — start php-fpm as the pre-script (it reads `/etc/php84/php-fpm.conf` with `daemonize=no` but we run it before httpd; the framework's EXEC_PRE_SCRIPT pattern handles this). +- `IS_WEB_SERVER="yes"`, `USES_DATABASE_SERVICE="yes"`, `DATABASE_SERVICE_TYPE="mariadb"` +- `WWW_ROOT_DIR="/usr/local/share/ampache/public"`, `ETC_DIR="/etc/apache2"`, `CONF_DIR="/config/apache2"` +- `ADDITIONAL_CONFIG_DIRS="/config/php84 /config/my.cnf.d /config/ampache"` +- `SERVICE_PORT="80"` +- `__execute_prerun_local`: ensure runtime dirs (`/run/apache2`, `/run/php-fpm`, `/run/mysqld`, `/tmp/php-sessions`, `/data/logs/{apache2,php-fpm,mariadb,ampache}`) exist with right ownership; chown `/usr/local/share/ampache/{config,channel,rest,play}` to `apache:apache` for the install.php writes; symlink `/usr/local/share/ampache/config/ampache.cfg.php` -> `/config/ampache/ampache.cfg.php` if not present. +- `__pre_execute_local`: wait for mariadb socket to appear (`/run/mysqld/mysqld.sock`) up to 30s before starting apache. +- `__update_conf_files_local`: replace `REPLACE_TZ` token in `/etc/php84/php.ini` with `$TZ`. (No tokens in apache or my.cnf — paths are baked in.) + +## 05-custom.sh additions + +Replace the current placeholder content (which only `mkdir`s and creates an empty webapp dir) with: + +1. Wipe distro-default `/etc/{apache2,php84,my.cnf.d}/*` (drop conf.d/{default,info,languages,mpm,userdir}.conf, ssl.conf etc.) so only our shipped files remain after `cp -Rf /tmp/etc/...`. + ```sh + for d in apache2 php84 my.cnf.d; do + [ -d /tmp/etc/$d ] || continue + rm -Rf /etc/$d/* + cp -Rf /tmp/etc/$d/. /etc/$d/ + done + ``` +2. Fetch + install Ampache 7.9.3 prebuilt zip: + ```sh + AMPACHE_VERSION="7.9.3" + AMPACHE_URL="https://github.com/ampache/ampache/releases/download/${AMPACHE_VERSION}/ampache-${AMPACHE_VERSION}_all_php8.4.zip" + mkdir -p /usr/local/share/ampache + cd /tmp + wget -q -O /tmp/ampache.zip "$AMPACHE_URL" + unzip -q /tmp/ampache.zip -d /usr/local/share/ampache + rm -f /tmp/ampache.zip + chown -R apache:apache /usr/local/share/ampache + # ensure config dir exists for install.php to drop ampache.cfg.php + mkdir -p /usr/local/share/ampache/config + chown apache:apache /usr/local/share/ampache/config + ``` +3. Create runtime dirs needed by apache/php-fpm/mariadb so the first start doesn't trip on missing parents: + ```sh + mkdir -p /run/apache2 /run/php-fpm /run/mysqld /var/log/apache2 /tmp/php-sessions + ``` +4. Stage seed `template-files/config/ampache/` with an empty `.gitkeep` so `__initialize_config_dir` creates `/config/ampache/` on first run (the real `ampache.cfg.php` is written by install.php). + +## 04-users.sh additions + +The `mariadb` Alpine package creates the `mysql` user automatically; the `apache2` package creates `apache`. So 04-users.sh stays mostly empty — but add a defensive `addgroup -S apache 2>/dev/null; adduser -S -G apache -h /var/www -s /sbin/nologin apache 2>/dev/null` block in case package ordering doesn't guarantee them. + +## 02-packages.sh additions + +Empty (no per-package compile or pip step needed; the prebuilt ampache zip has all PHP deps inside `vendor/`). + +## Dockerfile changes + +- Update `BUILD_DATE` to `202605091200` (today, 2026-05-09 12:00). +- Replace `PACK_LIST` with the trimmed list above. +- Keep everything else (multi-stage, scratch final, ARGs, ENVs, volumes, healthcheck). +- No structural changes — the template's RUN steps already invoke 00–07 setup scripts in order. + +## .env.scripts changes + +- Sync `ENV_PACKAGES` to match the new `PACK_LIST` (single space separated, no double spaces). +- Leave `SERVICE_PORT="80"`, `EXPOSE_PORTS=""`, `PHP_VERSION="php84"`. + +## README updates + +Document the first-run workflow: visit `http://localhost:8080/` → ampache installer → fill in DB host `127.0.0.1`, user `ampache`, password from `/config/secure/auth/user/ampache_db_pass` (or read from `docker exec`), DB name `ampache` (already created by post-execute), web admin → enter desired admin user/email/pass → done. Note the volumes (`/config`, `/data`) and the music library mount pattern (`-v /path/to/music:/media:ro`). + +## Verification (success criteria) + +1. `cd /root/Projects/github/casjaysdevdocker/ampache && rm -f .build_failed && buildx run Dockerfile` succeeds for both `linux/amd64` and `linux/arm64`. Single retry permitted on transient network errors. +2. `docker run -d --rm --name test-ampache -p 18080:80 docker.io/casjaysdevdocker/ampache:latest` boots; after ~60s, `docker logs test-ampache | tail -50` shows no fatal errors and shows mariadb + php-fpm + httpd all started (look for "ready for connections", "fpm is running", "Apache/2... configured -- resuming normal operations"). +3. `curl -fsS -o /dev/null -w '%{http_code}' http://localhost:18080/` returns 200 or 302 (the install.php redirect). +4. `docker exec test-ampache ls /config/ampache/ /config/apache2/ /config/php84/ /data/db/mariadb/ /usr/local/share/ampache/public/index.php` — every path exists. +5. `docker exec test-ampache mariadb -u root -e 'SHOW DATABASES;'` lists `ampache`. +6. `docker stop test-ampache`. + +## Rollback + +If anything in this PLAN.md proves wrong, the existing files are recoverable from git (`git checkout -- rootfs/`). New files (init.d, tmp/etc) can be removed cleanly because they didn't exist before.