{"id":49761856,"url":"https://github.com/ycpss91255-docker/base","last_synced_at":"2026-06-10T08:00:33.197Z","repository":{"id":347404225,"uuid":"1193890190","full_name":"ycpss91255-docker/base","owner":"ycpss91255-docker","description":"Shared template for Docker container repos: scripts, tests, CI workflows","archived":false,"fork":false,"pushed_at":"2026-06-05T06:53:11.000Z","size":2763,"stargazers_count":0,"open_issues_count":17,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-05T07:29:53.383Z","etag":null,"topics":["bash","docker","template","tool"],"latest_commit_sha":null,"homepage":null,"language":"Shell","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ycpss91255-docker.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-03-27T17:29:26.000Z","updated_at":"2026-06-05T06:53:14.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ycpss91255-docker/base","commit_stats":null,"previous_names":["ycpss91255-docker/docker_template","ycpss91255-docker/template","ycpss91255-docker/base"],"tags_count":121,"template":false,"template_full_name":null,"purl":"pkg:github/ycpss91255-docker/base","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ycpss91255-docker%2Fbase","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ycpss91255-docker%2Fbase/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ycpss91255-docker%2Fbase/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ycpss91255-docker%2Fbase/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ycpss91255-docker","download_url":"https://codeload.github.com/ycpss91255-docker/base/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ycpss91255-docker%2Fbase/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34142643,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-10T02:00:07.152Z","response_time":89,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bash","docker","template","tool"],"created_at":"2026-05-11T09:06:05.971Z","updated_at":"2026-06-10T08:00:33.138Z","avatar_url":"https://github.com/ycpss91255-docker.png","language":"Shell","funding_links":[],"categories":[],"sub_categories":[],"readme":"# base\n\n[![CI](https://github.com/ycpss91255-docker/base/actions/workflows/self-test.yaml/badge.svg)](https://github.com/ycpss91255-docker/base/actions/workflows/self-test.yaml)\n[![codecov](https://codecov.io/gh/ycpss91255-docker/base/branch/main/graph/badge.svg)](https://codecov.io/gh/ycpss91255-docker/base)\n\n![Language](https://img.shields.io/badge/Language-Bash-blue?style=flat-square)\n![Testing](https://img.shields.io/badge/Testing-Bats-orange?style=flat-square)\n![ShellCheck](https://img.shields.io/badge/ShellCheck-Compliant-brightgreen?style=flat-square)\n![Coverage](https://img.shields.io/badge/Coverage-Kcov-blueviolet?style=flat-square)\n[![License](https://img.shields.io/badge/License-Apache--2.0-blue?style=flat-square)](./LICENSE)\n\nShared template for Docker container repos in the [ycpss91255-docker](https://github.com/ycpss91255-docker) organization.\n\n**[English](README.md)** | **[繁體中文](doc/readme/README.zh-TW.md)** | **[简体中文](doc/readme/README.zh-CN.md)** | **[日本語](doc/readme/README.ja.md)**\n\n---\n\n## Table of Contents\n\n- [TL;DR](#tldr)\n- [Prerequisites](#prerequisites)\n- [Overview](#overview)\n- [Quick Start](#quick-start)\n- [CI Reusable Workflows](#ci-reusable-workflows)\n- [Running Template Tests](#running-template-tests)\n- [Tests](#tests)\n- [Directory Structure](#directory-structure)\n\n---\n\n## TL;DR\n\n```bash\n# New repo from scratch: init + first commit + subtree + init.sh\nmkdir \u003crepo_name\u003e \u0026\u0026 cd \u003crepo_name\u003e\ngit init\ngit commit --allow-empty -m \"chore: initial commit\"\ngit subtree add --prefix=.base \\\n    https://github.com/ycpss91255-docker/base.git vX.Y.Z --squash\n./.base/init.sh\n\n# Upgrade to latest\njust upgrade-check   # check\njust upgrade         # pull + update version + workflow tag\n\n# Run CI\nmake -f Makefile.ci test   # ShellCheck + Bats + Kcov\njust                       # show all recipes\n```\n\n## Prerequisites\n\nContainer operations run through [`just`](https://github.com/casey/just) (the\ncommand runner) layered on Docker. Install both on the host before using the\n`just \u003cverb\u003e` entry point:\n\n- **Docker** + Docker Compose v2 (`docker compose`).\n- **just** -- any recent release works (the recipes use only variadic\n  parameters, supported since early versions). Install via a package manager\n  or the official installer:\n\n  ```bash\n  apt install just         # Debian 13+ / Ubuntu 24.04+\n  brew install just        # macOS / Linuxbrew\n  cargo install just       # from crates.io\n  # or the official prebuilt-binary installer:\n  curl --proto '=https' --tlsv1.2 -sSf https://just.systems/install.sh \\\n      | bash -s -- --to ~/.local/bin\n  ```\n\n  See the [official install guide](https://github.com/casey/just#installation)\n  for every method. If `just` is unavailable each recipe has a raw fallback\n  (`./script/\u003cverb\u003e.sh`, `./.base/upgrade.sh`) -- see [Quick Start](#quick-start).\n\n## Overview\n\nThis repo consolidates shared scripts, tests, and CI workflows used across all Docker container repos. Instead of maintaining identical files in 15+ repos, each repo pulls this template as a **git subtree** and uses symlinks.\n\n### Architecture\n\n```mermaid\ngraph TB\n    subgraph base[\"base (shared repo)\"]\n        scripts[\".hadolint.yaml / Makefile.ci / compose.yaml\"]\n        smoke[\"test/smoke/\u003cbr/\u003escript_help.bats\u003cbr/\u003edisplay_env.bats\"]\n        config[\"config/\u003cbr/\u003ebashrc / tmux / terminator / pip\"]\n        mgmt[\"script/docker/\u003cbr/\u003ebuild.sh / run.sh / exec.sh / stop.sh / setup.sh\"]\n        workflows[\"Reusable Workflows\u003cbr/\u003ebuild-worker.yaml\u003cbr/\u003erelease-worker.yaml\u003cbr/\u003epublish-worker.yaml (opt-in)\"]\n    end\n\n    subgraph consumer[\"Docker Repo (e.g. ros_noetic)\"]\n        symlinks[\"justfile → .base/script/docker/justfile\u003cbr/\u003ebuild.sh → .base/script/docker/wrapper/build.sh\u003cbr/\u003erun.sh / exec.sh / stop.sh / prune.sh / setup.sh / setup_tui.sh\u003cbr/\u003e.hadolint.yaml\"]\n        dockerfile[\"Dockerfile\u003cbr/\u003ecompose.yaml\u003cbr/\u003escript/entrypoint.sh\"]\n        repo_test[\"test/smoke/\u003cbr/\u003eapp_env.bats (repo-specific)\"]\n        main_yaml[\"main.yaml\u003cbr/\u003e→ calls reusable workflows\"]\n    end\n\n    base -- \"git subtree\" --\u003e consumer\n    scripts -. symlink .-\u003e symlinks\n    smoke -. \"Dockerfile COPY\" .-\u003e repo_test\n    workflows -. \"@tag reference\" .-\u003e main_yaml\n```\n\n### CI/CD Flow\n\n```mermaid\nflowchart LR\n    subgraph local[\"Local\"]\n        build_test[\"./build.sh test\"]\n        make_test[\"just build test\"]\n    end\n\n    subgraph ci_container[\"CI Container (ghcr.io/ycpss91255-docker/test-tools:latest)\"]\n        shellcheck[\"ShellCheck\"]\n        hadolint[\"Hadolint\"]\n        bats[\"Bats smoke tests\"]\n    end\n\n    subgraph github[\"GitHub Actions\"]\n        build_worker[\"build-worker.yaml\u003cbr/\u003e(from template)\"]\n        release_worker[\"release-worker.yaml\u003cbr/\u003e(from template)\"]\n    end\n\n    build_test --\u003e ci_container\n    make_test --\u003e|\"script/ci/ci.sh\"| ci_container\n    shellcheck --\u003e hadolint --\u003e bats\n\n    push[\"git push / PR\"] --\u003e build_worker\n    build_worker --\u003e|\"docker build test\"| ci_container\n    tag[\"git tag v*\"] --\u003e release_worker\n    release_worker --\u003e|\"tar.gz + zip\"| release[\"GitHub Release\"]\n```\n\n### What's included\n\n| File | Description |\n|------|-------------|\n| `build.sh` | Build containers (TTY-aware `--setup` launches `setup_tui.sh`, else runs `setup.sh`) |\n| `run.sh` | Run containers (X11/Wayland support; same `--setup` semantics as `build.sh`; `--build` opt-in pre-flight ./build.sh test for fresh-clone CI parity) |\n| `exec.sh` | Exec into running containers |\n| `stop.sh` | Stop and remove containers |\n| `prune.sh` | Prune dangling images / build cache for the repo |\n| `setup_tui.sh` | Interactive setup.conf editor (dialog / whiptail front-end) |\n| `script/docker/wrapper/setup.sh` | Auto-detect system parameters and generate `.env` + `compose.yaml` |\n| `script/docker/lib/_lib.sh` | Core wrapper library (`_load_env`, `_compose`, `_compose_project`, ...) |\n| `script/docker/lib/bootstrap.sh` | Common wrapper initialization and arg parsing |\n| `script/docker/lib/compose.sh` | Docker Compose YAML generation and manipulation |\n| `script/docker/lib/conf.sh` | INI file parser and section merger |\n| `script/docker/lib/conf_logging.sh` | Logging configuration helpers |\n| `script/docker/lib/env.sh` | Environment variable setup and defaults |\n| `script/docker/lib/gitignore.sh` | Gitignore file management |\n| `script/docker/lib/hook.sh` | Per-wrapper pre/post hook invocation |\n| `script/docker/lib/i18n.sh` | Language detection and localization (`_detect_lang`, `_LANG`) |\n| `script/docker/lib/log.sh` | Unified logging and output utilities |\n| `script/docker/lib/config_summary.sh` | Summary of runtime configuration |\n| `script/docker/lib/_tui_backend.sh` | dialog/whiptail wrapper functions used by `setup_tui.sh` |\n| `script/docker/lib/_tui_conf.sh` | INI validators + read/write for `setup_tui.sh` and `setup.sh` writeback |\n| `script/docker/runtime/logging.sh` | Host-side log tee helper |\n| `script/docker/runtime/smoke.sh` | Runtime install-check smoke |\n| `script/docker/runtime/entrypoint.sh` | Template entrypoint helper |\n| `script/ci/ci.sh` | CI orchestration (local + remote) |\n| `script/ci/lint_bare_stderr.sh` | Bare stderr lint checker |\n| `script/ci/lint_mixed_test_layout.sh` | Mixed-tool test layout validator |\n| `config/` | Container-internal shell configs (bashrc, tmux, terminator) |\n| `setup.conf` | Single per-repo runtime configuration (image / build / deploy / gui / network / volumes) |\n| `test/smoke/` | Shared smoke tests + runtime assertion helpers (see below) |\n| `test/unit/` | Template self-tests (bats + kcov) |\n| `test/integration/` | Level-1 `init.sh` end-to-end tests |\n\nMulti-tool downstream repos (e.g. `.bats` + `pytest` in one category)\nsegregate by a `\u003ctool\u003e` subdir -- `test/\u003ccategory\u003e/\u003ctool\u003e/` (e.g.\n`test/smoke/bats/`, `test/smoke/pytest/`). Single-tool repos stay flat.\nSee [ADR-00000004](doc/adr/00000004-test-category-tool-subdir-layout.md).\n\n| `.hadolint.yaml` | Shared Hadolint rules |\n| `justfile` | Repo entry — `just \u003cverb\u003e` recipes (`just build`, `just run`, `just stop`, etc.). Sub-cmds and flags pass straight through as `{{args}}` (`just build --no-cache test`); `just` with no recipe lists all recipes. |\n| `Makefile.ci` | Template CI entry (`make -f Makefile.ci test`, `make -f Makefile.ci lint`, etc.). The user-facing vs CI-facing split is intentional. |\n| `init.sh` | First-time symlink setup + new-repo scaffolding |\n| `upgrade.sh` | Subtree version upgrade |\n| `dockerfile/Dockerfile.example` | Multi-stage Dockerfile template for new repos |\n| `dockerfile/Dockerfile.test-tools` | Pre-built lint/test tools image (shellcheck, hadolint, bats, bats-mock) |\n| `.github/workflows/` | Reusable CI workflows (build + release) |\n\n### Wrapper UX cheat sheet (#291)\n\nSingle canonical reference for what each user-facing script accepts.\nDownstream READMEs link here instead of duplicating the table.\n\n| Flag / form | `build.sh` | `run.sh` | `exec.sh` | `stop.sh` | `setup.sh` (CLI) |\n|---|:---:|:---:|:---:|:---:|:---:|\n| `-h` / `--help` | yes | yes | yes | yes | yes |\n| `-C` / `--chdir DIR` | yes | yes | yes | yes | — |\n| `--lang LANG` | yes | yes | yes | yes | yes |\n| `--dry-run` | yes | yes | yes | yes | — |\n| `-s` / `--setup` | yes | yes | — | — | — (target of `--setup`) |\n| `-t` / `--target TARGET` | yes (#280, alias to positional) | yes | yes | — (Q2: stays project-wide) | — |\n| `--instance NAME` | — (build-time concept) | yes | yes | yes | — |\n| `-q` / `--quiet` | — | — | — | — | yes (#285, on mutating subcommands) |\n| `--gui auto\\|force\\|off` | yes (#338) | yes (#338) | — | — | yes (apply, #338) |\n| `--no-x11-cookie` | yes (#338) | yes (#338) | — | — | yes (apply, #338) |\n| `--print-resolved` | — | — | — | — | yes (apply, #338) |\n| `--` separator | — | yes | yes (#289) | — | yes (per subcommand) |\n| Positional meaning | TARGET | CMD | CMD | `docker compose down` pass-through | subcommand name |\n\nDesign decisions locked by #291:\n\n- **Q1** (build.sh positional vs flag): keep positional + `-t` / `--target` as a backwards-compatible alias. `./build.sh runtime` and `./build.sh -t runtime` both work; downstream READMEs may use either, but should prefer the flag form for parity with `run.sh` / `exec.sh`.\n- **Q2** (stop.sh `-t`): not adopted. `stop.sh` stays project-wide (`docker compose down`), since per-service stop has different docker-side semantics (`docker compose stop \u003cservice\u003e`) and would conflate two cleanup verbs under one flag. Users wanting per-service control call `docker compose stop \u003cservice\u003e` directly.\n- **Q3** (setup.sh positional): subcommand-first verb-style (`./setup.sh set \u003ckey\u003e \u003cvalue\u003e`), unchanged. Different mental model from the wrapper trio's TARGET / CMD, matching `git` / `docker` CLI convention.\n\n### Dockerfile stages (convention)\n\nDownstream repos follow a standard multi-stage layout, defined in\n`dockerfile/Dockerfile.example`. All stages share a common base image\nparameterized by `ARG BASE_IMAGE`.\n\n| Stage | Parent | Purpose | Shipped? |\n|-------|--------|---------|----------|\n| `sys` | `${BASE_IMAGE}` | User/group, sudo, timezone, locale, APT mirror | intermediate |\n| `base` | `sys` | Development tools and language packages | intermediate |\n| `devel` | `base` | App-specific tools + `entrypoint.sh` + PlotJuggler (env repos) | **yes** (primary artifact) |\n| `test` | `devel` | Ephemeral: ShellCheck + Hadolint + Bats smoke (all from `test-tools:local`) | no (discarded) |\n| `runtime-base` (optional) | `sys` | Minimal runtime deps (sudo, tini) | intermediate |\n| `runtime` (optional) | `runtime-base` | Slim runtime image (application repos only) | yes, when enabled |\n\nNotes:\n- Repos that only ship a developer image (`env/*`) skip `runtime-base` /\n  `runtime` — the section stays commented in `Dockerfile.example`.\n- `test` is always built from `devel`, so runtime assertions inside\n  `test/smoke/\u003crepo\u003e_env.bats` see the same binaries / files a user would\n  find after `docker run ... \u003crepo\u003e:devel`.\n- `Dockerfile.test-tools` builds the lint/test tool bundle (bats + shellcheck +\n  hadolint). The downstream `test` stage consumes it through an `ARG\n  TEST_TOOLS_IMAGE` build arg — defaults to `test-tools:local` (matches the\n  local `./build.sh` flow that builds `Dockerfile.test-tools` into the host\n  Docker daemon). CI overrides it to\n  `ghcr.io/ycpss91255-docker/test-tools:vX.Y.Z` (pre-built multi-arch image\n  pushed by `.github/workflows/release-test-tools.yaml` on every tag) so\n  buildx pulls the arch-correct binaries over the wire instead of rebuilding\n  them per run, and sidesteps the cross-step image-store isolation that\n  `docker-container` buildx drivers enforce.\n\n#### Adding extra stages (#215)\n\nAny `FROM \u003cbase\u003e AS \u003cstage\u003e` outside the baseline blocklist\n`{sys, devel-base, devel, devel-test, runtime-test}` (legacy\n`{base, test}` also accepted during the v0.21.x transition) is\nauto-emitted as a compose service that\n`extends: devel` (inherits volumes / network / GPU / GUI / cap_add /\nadditional_contexts) and overrides only `build.target` / `image` /\n`container_name` / `stdin_open` / `tty` / `profiles`. Use case:\nentrypoint variants like NVIDIA Isaac Sim's `headless` + `gui` on top\nof `devel`.\n\nUser flow:\n\n```dockerfile\n# Add to Dockerfile (no setup.conf change needed)\nFROM devel AS headless\nENTRYPOINT [\"/isaac-sim/runheadless.sh\"]\nCMD [\"-v\"]\n\nFROM devel AS gui\nENTRYPOINT [\"/isaac-sim/runapp.sh\"]\n```\n\n```bash\njust build                            # regenerates compose.yaml, builds all stages\njust run -t headless                  # runs the headless variant\njust run -t gui                       # runs the gui variant\njust exec -t headless bash            # exec into running headless container\n\n# Kit-style args (containing `=`) pass straight through as recipe\n# arguments — no env-var workaround needed:\njust exec -t headless-stream /isaac-sim/runheadless.sh -v --/app/livestream/port=49100\n\n# Equivalent direct .sh invocation:\n./build.sh\n./run.sh -t headless\n./exec.sh -t headless bash\n```\n\nConstraints:\n\n- Stage names must match `^[a-z][a-z0-9_-]*$` — uppercase / leading\n  digit / dot etc. are rejected (WARN + skip; the rest of the parse\n  continues).\n- Names colliding with the baseline (`sys` / `devel-base` / `devel`\n  / `runtime-test`, plus legacy aliases `base` / `test` during the\n  v0.21.x transition) are a hard error from `setup.sh apply`. So are\n  names colliding with the template-controlled image-tag namespace\n  (`latest`, `v[0-9]*`). `devel-test` is **not** a collision — it is\n  emitted as the `test` service through the per-stage model (#493, see\n  below).\n- Adding / removing a stage triggers `setup.sh check-drift` (via\n  `SETUP_DOCKERFILE_HASH` in `.env`), so wrappers auto-regenerate\n  `compose.yaml` on the next invocation. Unrelated `RUN apt-get\n  install` edits do **not** trigger drift.\n\n#### Per-stage `setup.conf` overrides (#220)\n\nStages auto-emitted by #215 share devel's runtime config (volumes /\nGPU / network / GUI) by default. When a stage needs different runtime\nsettings — e.g. NVIDIA Isaac Sim's `headless` running a WebRTC\nlivestream wants `network=bridge` + a port mapping + `gui=off`, while\n`devel` and `gui` keep `network=host` + X11 — add a `[stage:\u003cname\u003e]`\nsection to your repo's `setup.conf`:\n\n```ini\n[gui]\nmode = auto\n\n[network]\nmode = host\n\n[stage:headless]\ngui.mode = off\nnetwork.mode = bridge\nnetwork.port_1 = 8080:80\ndeploy.gpu_capabilities = gpu compute utility graphics video\n```\n\nUse `./setup_tui.sh` for an interactive editor:\n\n- **Advanced → Per-stage overrides**: drills straight into the editor.\n  The entry only appears when your Dockerfile has at least one\n  non-baseline stage.\n- **Features → Per-stage overrides** (#221): always-visible\n  discoverability surface that lists conditional / power-user\n  features. When the precondition is met it acts as a shortcut into\n  the same editor; when not, it pops a msgbox explaining how to\n  enable.\n\nAllowlist (v1 — keys that can be overridden per-stage):\n\n| Section | Keys |\n|---|---|\n| `[deploy]` | `gpu_mode`, `gpu_count`, `gpu_capabilities`, `gpu_runtime` (legacy `runtime` still accepted) |\n| `[gui]` | `mode` |\n| `[network]` | `mode`, `ipc`, `pid`, `network_name`, `port_\u003cN\u003e`, `port_inherit` |\n| `[security]` | `privileged`, `cap_add_\u003cN\u003e`, `cap_add_inherit`, `cap_drop_\u003cN\u003e`, `cap_drop_inherit`, `security_opt_\u003cN\u003e`, `security_opt_inherit` |\n| `[volumes]` | `mount_\u003cN\u003e`, `mount_inherit` |\n| `[environment]` | `env_\u003cN\u003e`, `env_inherit` |\n\nList fields (`mount_*` / `port_*` / `env_*` / `cap_add_*` / `cap_drop_*`\n/ `security_opt_*`) follow **append-default**: the stage's items are\nappended to top-level entries. To replace top-level entirely, set\n`\u003clist\u003e_inherit = false` (e.g. `volumes.mount_inherit = false`, or\n`security.cap_add_inherit = false` to drop a stage's inherited caps —\n#526: a read-only probe stage clears the flash stage's `SYS_ADMIN`).\n\nNotes:\n\n- `[stage:devel]` is **reserved** (v1 no-op + WARN). Edit top-level\n  sections to tune devel. Revisit in v2.\n- `[stage:devel-test]` (#493) is the override surface for the **`test`\n  service** (the `devel-test` Dockerfile stage). By default `test`\n  `extends: devel` and inherits its runtime config; declare\n  `[stage:devel-test]` to diverge — e.g. `deploy.gpu_mode = force` to\n  give GPU-requiring runtime tests (Isaac Sim pytest) a GPU even when\n  devel has none. The service name stays `test` (`./script/exec.sh -t\n  test` unchanged); `build.target` stays `devel-test`.\n- `[stage:sys|base|test]` is a **hard error** (baseline collision) —\n  use `[stage:devel-test]` to control the test service, not\n  `[stage:test]`.\n- `[stage:foo]` referencing a stage absent from the Dockerfile is\n  **WARN + skipped** (the rest of `setup.sh apply` continues).\n- Override keys outside the allowlist are **WARN + skipped per-key**.\n\n### Smoke test helpers (for downstream repos)\n\n`test/smoke/test_helper.bash` (loaded by every smoke spec via\n`load \"${BATS_TEST_DIRNAME}/test_helper\"`) ships a small set of runtime\nassertion helpers. Downstream repos should prefer these over ad-hoc\n`[ -f ... ]` / `command -v` checks so failures produce decorated\ndiagnostics pointing at the missing artifact.\n\n| Helper | Usage |\n|--------|-------|\n| `assert_cmd_installed \u003ccmd\u003e` | Fails unless `\u003ccmd\u003e` is on `PATH` |\n| `assert_cmd_runs \u003ccmd\u003e [flag]` | Fails unless `\u003ccmd\u003e \u003cflag\u003e` exits 0 (default flag: `--version`) |\n| `assert_file_exists \u003cpath\u003e` | Fails unless `\u003cpath\u003e` is a regular file |\n| `assert_dir_exists \u003cpath\u003e` | Fails unless `\u003cpath\u003e` is a directory |\n| `assert_file_owned_by \u003cuser\u003e \u003cpath\u003e` | Fails unless `\u003cpath\u003e`'s owner is `\u003cuser\u003e` |\n| `assert_pip_pkg \u003cpkg\u003e` | Fails unless `pip show \u003cpkg\u003e` returns 0 |\n\n### What stays in each repo (not shared)\n\n- `Dockerfile`\n- `compose.yaml`\n- `script/` — repo-local runtime helpers (invoked inside the container by `ENTRYPOINT` / `CMD` or by hand)\n  - `script/entrypoint.sh` (canonical)\n  - any ros / app launch helpers etc.\n- `script/docker/` — repo-local Dockerfile-internal build helpers (invoked from a Dockerfile `RUN`, never inside a running container; see commented stub + lint COPY in `dockerfile/Dockerfile.example`, #275)\n- `doc/` and `README.md`\n- Repo-specific smoke tests\n\n## Per-repo runtime configuration\n\nEach downstream repo drives its runtime config — GPU reservation, GUI\nenv/volumes, network mode, extra volume mounts — through a single\n`setup.conf` INI file. `setup.sh` reads it (plus system detection) and\nregenerates both `.env` and `compose.yaml`; users never hand-edit those\ntwo derived artifacts.\n\n### One conf, seven sections\n\n```\n[image]    rules = prefix:docker_, suffix:_ws, @default:unknown\n[build]    apt_mirror_ubuntu, apt_mirror_debian            # Dockerfile build args\n[deploy]   gpu_mode (auto|force|off), gpu_count, gpu_capabilities\n           dri_groups (auto|off) — iGPU /dev/dri group_add on GUI svcs\n[lifecycle] restart (no|always|unless-stopped|on-failure|on-failure:N)\n           default no; on devel (extends:devel stages inherit). Avoid\n           always/unless-stopped on stages that exit 0 (infinite restart).\n[gui]      mode (auto|force|off)\n[network]  mode (host|bridge|none), ipc, pid (host|private), privileged\n[volumes]  mount_1 (workspace, auto-populated on first run)\n           mount_2..mount_N (extra host mounts; devices via /dev path)\n[logging]  driver (json-file default), max_size, max_file, compress\n           local_path (host-side log dir; bind-mounted to /var/log/\u003crepo\u003e)\n           [logging.\u003csvc\u003e] for per-service key-level override\n```\n\nTemplate default lives at `.base/config/docker/setup.conf`\n(post-v0.25.0); per-repo overrides go at `\u003crepo\u003e/config/docker/setup.conf`.\nSection-level **replace** strategy: a section present in the per-repo\nfile fully replaces the template's section; omitted sections fall back\nto template.\n\n**Privileges are opt-in** (#466): the template ships lean `[security]`\n(`privileged = false`, no `cap_add` / `security_opt`) and `[devices]`\n(no `/dev:/dev`) defaults, so lightweight repos and tooling stages stay\nclean. Enable what a container needs via `setup_tui.sh` (security /\ndevices pages), `setup.sh add security.cap_add SYS_ADMIN`, or by\nuncommenting the examples in the template.\n\nOn first `setup.sh` run (no per-repo setup.conf yet), the template file\nis copied to `\u003crepo\u003e/config/docker/setup.conf` (the parent dir is created\nautomatically) and the detected workspace is written to `[volumes]\nmount_1`. Subsequent runs read `mount_1` as source of truth — clear it\nto opt out of mounting a workspace. Edit via:\n\n```bash\n./setup_tui.sh                      # interactive dialog/whiptail editor\n./setup_tui.sh volumes              # jump directly to one section\n./build.sh --setup            # launches setup_tui.sh under TTY; setup.sh otherwise\n./.base/init.sh --gen-conf # plain copy of .base/config/docker/setup.conf\n                              # to \u003crepo\u003e/config/docker/setup.conf\n```\n\n### Where each parameter lives (env vs workload)\n\nNot every runtime value belongs in `setup.conf`. The dividing question\n(axis A, [ADR-00000003](doc/adr/00000003-env-vs-workload-param-boundary.md))\nis **\"does this value change when you switch machines?\"** -- if yes it is\n*environment* (machine-bound, stays in `setup.conf`); if it changes per\ntask it is *workload*. \"Does it need a rebuild?\" (axis C) breaks grey\ncases. Three channels carry these values, and only the first survives\ninto a field deployment that ships just the image:\n\n| Parameter kind | Examples | Where it lives | Dev host | Field |\n|---|---|---|---|---|\n| machine-bound / set-once | GPU reservation, `privileged`, device/volume mounts, `IMAGE_NAME`, APT mirror | `setup.conf` (committed) | rendered into `compose.yaml` | inlined as `docker run` flags in a generated `deploy.sh` |\n| volatile workload **env vars** | `ROS_DOMAIN_ID`, `LOG_LEVEL`, API tokens, dataset selectors | `.env` overlay (hand-authored, gitignored) | injected via `env_file` on top of the generated cache (later file wins) | baked `ENV` defaults (+ optional launcher `-e`) |\n| structured app **config** | bridge topic lists, pipeline definitions | `config/app/` (#504) | bind-mounted at `/opt/app/config` (edit + restart, no rebuild) | `COPY`-baked into the image |\n\n`setup.conf`'s `[environment]` section is the *first* kind -- stable,\nmachine-bound env defaults that get baked into the runtime image as\n`ENV`. Put per-task env vars in the `.env` overlay instead, so a tweak\nneeds only `just run` (no `compose.yaml` regenerate, no `SETUP_CONF_HASH`\ndrift, no git churn).\n\n\u003e The `.env` overlay, the runtime-stage `ENV` bake, and the generated\n\u003e `deploy.sh` field launcher are rolling out across the\n\u003e [#497](https://github.com/ycpss91255-docker/base/issues/497) epic; this\n\u003e section documents the routing model they implement.\n\n### Field deployment (`setup.sh deploy`)\n\n`./setup.sh deploy` builds a self-contained field bundle from the same\n`setup.conf` -- the deploy half of the routing model above. It targets a\nstage (default `runtime`) and produces a single `tar.xz` carrying just two\nthings: the immutable image and a generated `deploy.sh` launcher.\n\n```bash\n./setup.sh deploy                       # build runtime bundle (prompts first)\n./setup.sh deploy --dry-run             # print the build plan, build nothing\n./setup.sh deploy --stage runtime -y    # skip the confirmation prompt\n./setup.sh deploy -o /tmp/robot.tar.xz  # custom output path\n```\n\nWhat it does, in order:\n\n1. bake the `[environment]` defaults into the image as real `ENV` (S3) and\n   `COPY` `config/app/` into it when present (S4) -- so the field image is\n   self-contained (no env file, no config bind travels);\n2. `docker build --target \u003cstage\u003e` the immutable image;\n3. generate `deploy.sh` -- a `docker run` launcher with every machine-bound\n   docker-level flag inlined (privileged / gpus / runtime / network / ipc /\n   pid / devices / caps / shm / restart / group-add), resolved from the\n   chosen stage exactly as `compose.yaml` would for dev;\n4. `docker save` the image and `tar -cJf` `{image.tar, deploy.sh}` into the\n   bundle.\n\nBefore building, it prints the resolved launcher so you can review every\ninlined flag, then prompts (skip with `-y`; `--dry-run` prints the plan and\nthe launcher without building; a non-interactive shell without `-y`\nrefuses). On the field machine:\n\n```bash\ntar -xJf \u003cname\u003e-runtime.tar.xz\ndocker load \u003c image.tar\n./deploy.sh                 # or: DEPLOY_IMAGE=... DEPLOY_CONTAINER_NAME=... ./deploy.sh\n```\n\nThe launcher carries docker-level flags only by design: workload env vars\nare baked `ENV` (override at run time with `-e` after `./deploy.sh`), and\nthe dev workspace bind is intentionally dropped (the field image ships its\nown code). `--group-add` GIDs (iGPU `/dev/dri`) are read from the\ngenerating host and may need adjusting on a different field machine.\n\n### Logging output to host\n\nSet `[logging] local_path` to tee container stdout/stderr to a host-side\nfile, in addition to the docker daemon's json-file log:\n\n```ini\n[logging]\nlocal_path = ./log/   # repo-relative; or /abs/, or ~/dir/\n```\n\nRe-run any wrapper to regenerate `compose.yaml`. Host file lands at\n`\u003clocal_path\u003e/\u003csvc\u003e.log` per service. `docker logs \u003cct\u003e` is unaffected\n(json-file keeps rolling history; the host file mirrors the current\nrun).\n\nFor **new repos** generated with `init.sh` from this version on, the\nhelper is pre-wired in `script/entrypoint.sh` — setting\n`[logging] local_path` is the only step. For **existing repos**, add\nthis single un-guarded line to `script/entrypoint.sh` before the\nfinal `exec` as a one-time migration:\n\n```bash\n. /usr/local/lib/base/_entrypoint_logging.sh\n```\n\nThe helper is COPY'd into the image at the stable in-image path\n`/usr/local/lib/base/_entrypoint_logging.sh` by `Dockerfile.example`'s\ndevel stage (refs #368), so the source line works at build-time AND\nruntime in every workspace layout — no `$USER` deref, no workspace\nbind-mount dependence.\n\nTroubleshooting: `local_path` set but the host file stays empty →\ncheck `script/entrypoint.sh` actually contains the source line\n(`grep _entrypoint_logging script/entrypoint.sh`).\n\n### Interactive TUI\n\n`./setup_tui.sh` opens the main menu. The backend is `dialog` or `whiptail` (when both are missing it prints a `sudo apt install dialog` hint and exits). Cancel / Esc leaves without saving; saving auto-invokes `setup.sh` to regenerate `.env` + `compose.yaml`.\n\nMain menu structure (#221):\n\n```\nMain\n├─ image            IMAGE_NAME detection rules\n├─ build            APT mirrors + Dockerfile build args\n├─ Runtime  ──→     network / deploy (GPU) / gui / environment / logging\n├─ Mounts   ──→     volumes / devices / tmpfs\n├─ Advanced ──→     security / additional_contexts\n│                   / per_stage (conditional) / Reset\n├─ Features         conditional / power-user features index\n│                   (today: per_stage status row)\n└─ Save \u0026 Exit\n```\n\n`./setup_tui.sh \u003csection\u003e` still drills directly into a section editor (e.g. `./setup_tui.sh volumes`), bypassing the main menu.\n\n### When setup.sh runs\n\n`setup.sh` runs only when explicitly triggered — it is not re-run on\nevery build or launch:\n\n- **`./.base/init.sh`** runs it once after the skeleton lands\n- **`just upgrade` / `./.base/upgrade.sh`** re-runs it via init.sh\n  after the subtree pull, so an upgrade always lands with `.env` /\n  `compose.yaml` regenerated against the new baseline\n- **`./build.sh --setup` / `./run.sh --setup`** (or `-s`) re-runs it on demand\n- **First-time bootstrap**: `./build.sh` / `./run.sh` auto-run setup.sh\n  the very first time (when `.env` is missing, e.g. after a fresh CI\n  clone) — no manual `--setup` needed\n\n\u003e **Fresh-clone lint coverage (#216)**: `./run.sh` on a clone with no\n\u003e image cached locally triggers Compose's auto-build, which only walks\n\u003e `target: devel` (or whatever `-t` says) and **skips** the\n\u003e `target: devel-test` stage that runs ShellCheck / Hadolint / Bats\n\u003e smoke (pre-#243 this stage was named `test`). `run.sh`\n\u003e prints an informational `[run] INFO:` block when this is about to\n\u003e happen (TTY only). Pass `--build` to pre-flight `./build.sh test`\n\u003e first if you want full local-CI parity in one command:\n\u003e\n\u003e ```bash\n\u003e just build test                   # explicit lint + smoke pass\n\u003e just run --build                  # same, then compose up\n\u003e just run                          # default — fast path, lint/smoke skipped\n\u003e ```\n\n`setup.sh apply` rewrites `compose.yaml` from scratch every time but\npreserves `WS_PATH` / `APT_MIRROR_UBUNTU` / `APT_MIRROR_DEBIAN` from any\nexisting `.env`, so a hand-tuned workspace path or apt mirror survives\nupgrades.\n\n### Drift detection\n\n`setup.sh` stores `SETUP_CONF_HASH`, `SETUP_GUI_DETECTED`, and\n`SETUP_TIMESTAMP` in `.env`. On every `./build.sh` / `./run.sh`,\nstored values are compared against the current setup.conf hash + system\ndetection; a `[WARNING]` is printed (non-blocking) when any of the\nfollowing changed since last setup:\n\n- `setup.conf` contents (conf hash)\n- GPU / GUI detection\n- `USER_UID` (user identity change)\n\nRe-run with `--setup` to regenerate `.env` + `compose.yaml`.\n\n### setup.sh subcommands (v0.11.0+)\n\n`setup.sh` is a git-style backend with explicit subcommands. The build / run / TUI scripts call it for you; invoke directly for scripted / non-interactive use:\n\n| Subcommand | Use |\n|---|---|\n| `apply` | Regenerate `.env` + `compose.yaml` from setup.conf + system detection |\n| `check-drift` | Exit 0 in-sync / 1 drifted (drift descriptions on stderr) |\n| `set \u003csection\u003e.\u003ckey\u003e \u003cvalue\u003e` | Write a single key |\n| `show \u003csection\u003e[.\u003ckey\u003e]` | Read single key or whole section |\n| `list [\u003csection\u003e]` | INI-style dump |\n| `add \u003csection\u003e.\u003clist\u003e \u003cvalue\u003e` | Append to list-style section (`mount_*` / `env_*` / `port_*` / …); reuses next empty slot or `max+1` |\n| `remove \u003csection\u003e.\u003ckey\u003e` / `\u003csection\u003e.\u003clist\u003e \u003cvalue\u003e` | Delete by exact key, or by value match |\n| `reset [-y\\|--yes]` | Restore template default; archives prior `setup.conf` → `setup.conf.bak`, prior `.env` → `.env.bak` |\n| `deploy [--stage S] [--output F] [--dry-run] [-y]` | Build a self-contained field bundle (`tar.xz` of image + generated `deploy.sh`) for stage `S` (default `runtime`); previews the resolved launcher and prompts before building. See [Field deployment](#field-deployment-setupsh-deploy) |\n\nTyped keys validate against `_tui_conf.sh` validators (the same ones the TUI uses). `set` / `add` / `remove` / `reset` do **not** regenerate `.env` — chain `apply` afterwards, or `build.sh` / `run.sh` will trigger drift-regen on next invocation.\n\n#### Migration from v0.10.x (BREAKING)\n\n`setup.sh` (no args) and `setup.sh --base-path X --lang Y` (no subcommand) used to silently fall through to `apply`. v0.11.0 removes that fall-through:\n\n| Invocation | Pre-v0.11 | v0.11+ |\n|---|---|---|\n| `setup.sh` | runs apply | prints help, exits 0 |\n| `setup.sh --base-path X --lang Y` | runs apply | exit 1 \"Unknown subcommand\" |\n| `setup.sh apply [...]` | runs apply | runs apply (unchanged) |\n\nIf a downstream repo has custom scripts invoking `setup.sh` directly, prepend `apply`. The bundled `build.sh` / `run.sh` / `init.sh` / `setup_tui.sh` are already updated.\n\n### Derived artifacts (gitignored)\n\n- `.env` — runtime variable values + `SETUP_*` drift metadata\n- `compose.yaml` — full compose with baseline + conditional blocks\n\nOpen `compose.yaml` anytime to inspect the repo's current effective\nconfiguration. Both files are regenerated on every `just upgrade`\n(init.sh re-runs `setup.sh apply` after the subtree pull) — never\nhand-edit them; put your overrides in `setup.conf` instead.\n\n### Per-wrapper hooks (#440)\n\nEvery wrapper (`run` / `build` / `exec` / `stop` / `prune` / `setup` /\n`setup_tui`) checks for an optional repo-local script at:\n\n```\nscript/hooks/pre/\u003cwrapper\u003e.sh    # runs after env prep, before main work\nscript/hooks/post/\u003cwrapper\u003e.sh   # runs after main work (or in EXIT trap for run.sh)\n```\n\n`init.sh` ships 14 executable stubs (`exit 0` by default), so the\nhook framework is ready out of the box. Replace `exit 0` with your\nown host-side steps (e.g. `multiarch/qemu-user-static` binfmt\nregistration, mount-point dir creation, hardware preflight). Stubs\nare idempotent across upgrades — pre-#440 templates pick up the\nscaffolding on the next `just upgrade`.\n\n**Contract:**\n\n| Aspect | Behavior |\n|---|---|\n| Args | Same `\"$@\"` the wrapper received |\n| Where | Host-side (NOT inside the container) |\n| `pre` non-zero | Aborts the wrapper |\n| `post` non-zero | Overrides wrapper exit code; cleanup still runs (run.sh) |\n| Not executable | Hard fail with `chmod +x` hint |\n| `--dry-run` | Both hooks silently skipped |\n\n**Example — jetson_sdk_manager binfmt setup:**\n\n```bash\n# script/hooks/pre/run.sh\n#!/usr/bin/env bash\nif [ ! -f /proc/sys/fs/binfmt_misc/qemu-aarch64 ]; then\n  docker run --rm --privileged \\\n    multiarch/qemu-user-static --reset -p yes\nfi\n```\n\n### Naming scheme: three namespaces, two user identities\n\n`setup.sh` emits three names in `.env` / `compose.yaml`. They look\nsimilar on a single-user dev machine, but they live in **three\ndifferent namespaces** and pick their user prefix from **two\ndifferent identities**. Sysadmins running shared hosts need to know\nthe difference; solo developers can treat the two identities as the\nsame and move on.\n\n| Name | Format | Namespace | User prefix |\n|---|---|---|---|\n| `image:` | `${DOCKER_HUB_USER:-local}/\u003crepo\u003e:\u003ctag\u003e` | **Registry** (Docker Hub) | `DOCKER_HUB_USER` |\n| `container_name:` | `${USER_NAME}-\u003crepo\u003e${INSTANCE_SUFFIX}` | **Host daemon** (per docker daemon, flat global) | `USER_NAME` (OS user, refs #322) |\n| compose project name | `${DOCKER_HUB_USER}-\u003crepo\u003e${INSTANCE_SUFFIX}` | **Host daemon** (drives default network / volume labels) | `DOCKER_HUB_USER` |\n\n- `DOCKER_HUB_USER` — your Docker Hub account, used to namespace\n  images on the registry side. Image tags are addressable as\n  `\u003cDOCKER_HUB_USER\u003e/\u003crepo\u003e:\u003ctag\u003e` whether or not you actually push.\n- `USER_NAME` — the OS user (from `id -un`), used to keep two OS\n  users on the same host from colliding on the daemon's flat\n  container-name namespace.\n\nThe two identities are deliberately separate. Image names use the\nDocker Hub identity because images are addressable on the registry,\nand forcing per-OS-user image tags would shatter buildx cache reuse\nand Docker Hub layer sharing. Container names use the OS identity\nbecause the conflict it fixes (two users on the same host running\nthe same repo) is a host-daemon problem with no registry component.\n\nProject-name choice of `DOCKER_HUB_USER` predates #322 and was kept\nunchanged: on a single-user dev machine the two identities coincide\nso the names line up visually with `container_name`; on a shared\nhost the project name still avoids cross-user collision *because*\n`DOCKER_HUB_USER` happens to differ per user too. The `#322`\nCHANGELOG entry's phrasing \"aligns container-level naming with\nproject-level naming\" is true under that single-user-machine\nassumption — both are user-prefixed, just via different vars — not\nliterally the same prefix string in the multi-user case.\n\n**`INSTANCE_SUFFIX`** is the fourth dimension, orthogonal to the\nuser split. Same OS user wants to run the same repo as multiple\nparallel containers (e.g. two branches side by side): set\n`INSTANCE_SUFFIX=2` and you get `alice-\u003crepo\u003e-2` /\n`alice-\u003crepo\u003e-2`-named project. Empty by default; bumped by the\n`-n / --instance` flag on the wrappers when applicable.\n\n**Per-instance overlays (#465).** When `run.sh --instance NAME` is\ngiven, `run.sh` also picks up these two optional files as compose\noverlays:\n\n```\nconfig/instances/\u003cNAME\u003e.yaml   → docker compose -f\nconfig/instances/\u003cNAME\u003e.env    → docker compose --env-file\n```\n\nEither file may exist alone; missing files are silently skipped. Use\nthe yaml for structural overrides (per-instance ports, volumes,\ncache dirs) and the env for pure `${VAR}` overrides shared with\n`compose.yaml`. `NAME` is validated as `^[a-z0-9][a-z0-9_-]*$` for\npath safety.\n\nWorked example. OS user `alice`, Docker Hub user `alice-hub`, repo\n`claude_code`, default `INSTANCE_SUFFIX` empty:\n\n```\nimage:          alice-hub/claude_code:devel\ncontainer_name: alice-claude_code\nproject name:   alice-hub-claude_code\n```\n\nSame OS user, second instance (`INSTANCE_SUFFIX=2`):\n\n```\nimage:          alice-hub/claude_code:devel        (unchanged — same image)\ncontainer_name: alice-claude_code-2\nproject name:   alice-hub-claude_code-2\n```\n\nA second OS user `bob` on the same host:\n\n```\nimage:          bob-hub/claude_code:devel          (different registry tag, no cache reuse)\ncontainer_name: bob-claude_code\nproject name:   bob-hub-claude_code\n```\n\nIf `alice` and `bob` share `DOCKER_HUB_USER` (e.g. a shared CI\nservice account), `image` collides on Docker Hub but `container_name`\nstill differentiates — registry pulls share the cached image and\nhosts stay deconflicted.\n\n## Quick Start\n\n### Adding to a new repo\n\n```bash\n# 1. Initialize empty repo (skip if you already have one with at least one commit)\nmkdir \u003crepo_name\u003e \u0026\u0026 cd \u003crepo_name\u003e\ngit init\ngit commit --allow-empty -m \"chore: initial commit\"\n\n# 2. Add subtree (pin a specific release tag, not a moving branch)\ngit subtree add --prefix=.base \\\n    https://github.com/ycpss91255-docker/base.git vX.Y.Z --squash\n\n# 3. Initialize symlinks (one command; runs setup.sh under the hood)\n./.base/init.sh\n```\n\n\u003e `git subtree add` requires `HEAD` to exist. On a freshly `git init`-ed repo with no commits, it fails with `ambiguous argument 'HEAD'` and `working tree has modifications`. The empty commit creates `HEAD` so subtree can merge into it.\n\n### Updating\n\nPrerequisites: `git config user.name` / `user.email` must be set, and\nthe working tree can't be mid-merge / rebase / cherry-pick / revert —\nupgrade.sh fails fast with an actionable message instead of half-pulling.\n\n```bash\n# Check if update available\njust upgrade-check\n\n# Upgrade to latest (subtree pull + version file + workflow tag)\njust upgrade\n\n# Or pin a specific version\njust upgrade v0.3.0\n# Pinning to a version OLDER than the current local pin (e.g. rolling\n# from v0.12.0-rc1 back to v0.11.0) is refused as an implicit downgrade\n# per SemVer §11. Edit .base/.version manually if intentional.\n\n# Fallback if just is unavailable\n./.base/upgrade.sh v0.3.0\n```\n\n`upgrade.sh` handles the full cycle in one go:\n\n1. `git subtree pull --prefix=.base ... --squash`\n2. Post-pull integrity check — `git reset --hard` rollback if subtree\n   markers (`.base/.version`, `.base/init.sh`,\n   `.base/script/docker/setup.sh`) are missing (catches the\n   destructive fast-forward seen on older `git-subtree.sh`)\n3. `./.base/init.sh` re-runs to: resync root symlinks\n   (`build.sh` / `run.sh` / `justfile` …), sync `.gitignore` against\n   the canonical entry set, `git rm --cached` any tracked-but-now-derived\n   files (`.env`, `compose.yaml`, …), and call `setup.sh apply` to\n   regenerate `.env` + `compose.yaml`\n4. `sed` rewrites `.github/workflows/main.yaml`'s\n   `build-worker.yaml@vX.Y.Z` / `release-worker.yaml@vX.Y.Z` refs\n\nYour per-repo files are never overwritten: `\u003crepo\u003e/config/docker/setup.conf` stays\nas-is, and `\u003crepo\u003e/config/` (bashrc / tmux / terminator …) is left\nalone — if upstream `.base/config/` moved since the last pull,\nupgrade.sh prints a `diff -ruN .base/config config` hint so you can\nreconcile manually.\n\nDon't `git subtree pull` by hand — the integrity check, init.sh\nresync, and sed steps are easy to forget.\n\n#### Automated version bumps (optional)\n\nDownstream repos can let Dependabot open PRs whenever a new `template` tag\nships. Add `.github/dependabot.yml`:\n\n```yaml\nversion: 2\nupdates:\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"weekly\"\n```\n\nDependabot notices the `uses: ycpss91255-docker/base/...@vX.Y.Z` refs in\n`main.yaml`, compares against the template's latest tag, and files a PR. You\nstill run `just upgrade vX.Y.Z` locally to sync the subtree itself —\nDependabot only bumps the workflow refs.\n\n## CI Reusable Workflows\n\nRepos replace local `build-worker.yaml` / `release-worker.yaml` with calls to this repo's reusable workflows:\n\n```yaml\n# .github/workflows/main.yaml\njobs:\n  call-docker-build:\n    uses: ycpss91255-docker/base/.github/workflows/build-worker.yaml@v1\n    with:\n      image_name: my_app\n      build_args: |\n        BASE_IMAGE=python:3.11-slim\n        APP_VERSION=1.0\n        DEBIAN_CODENAME=bookworm\n\n  call-release:\n    needs: call-docker-build\n    if: startsWith(github.ref, 'refs/tags/')\n    uses: ycpss91255-docker/base/.github/workflows/release-worker.yaml@v1\n    with:\n      archive_name_prefix: my_app\n```\n\n### build-worker.yaml inputs\n\n| Input | Type | Required | Default | Description |\n|-------|------|----------|---------|-------------|\n| `image_name` | string | yes | - | Container image name |\n| `build_args` | string | no | `\"\"` | Multi-line KEY=VALUE build args |\n| `build_runtime` | boolean | no | `true` | Whether to build runtime stage |\n| `platforms` | string | no | `\"linux/amd64\"` | Comma-separated target platforms; each runs as a parallel native-runner shard (`linux/amd64` → ubuntu-latest, `linux/arm64` → ubuntu-24.04-arm) |\n| `test_tools_version` | string | no | `\"latest\"` | Tag for `ghcr.io/ycpss91255-docker/test-tools:\u003ctag\u003e` build-arg; pin to the template release you upgraded from for reproducibility |\n\n### release-worker.yaml inputs\n\n| Input | Type | Required | Default | Description |\n|-------|------|----------|---------|-------------|\n| `archive_name_prefix` | string | yes | - | Archive name prefix |\n| `extra_files` | string | no | `\"\"` | Space-separated extra files |\n\n### publish-worker.yaml inputs (opt-in, foundational image repos)\n\nPushes a Dockerfile target stage to a container registry on tag push.\nOpt-in: only repos that consume this workflow publish images (default\ntemplate flow stays test-only). Typical use case: foundational image\nrepos that other repos consume via Docker `FROM`.\n\n| Input | Type | Required | Default | Description |\n|-------|------|----------|---------|-------------|\n| `image_name` | string | yes | - | Image repo name on the registry (e.g. `my_image`); full ref becomes `${registry}/${owner}/${image_name}` |\n| `tag_suffix` | string | no | `\"\"` | Appended to both `:${version}` and `:latest` tags. Convention: `-\u003cmatrix-entry-name\u003e` so each variant lands on its own tag |\n| `is_latest` | boolean | no | `false` | When true, also pushes `:latest${tag_suffix}` alongside `:${version}${tag_suffix}`. Multi-variant repos set this only on the canonical default variant |\n| `registry` | string | no | `\"ghcr.io\"` | Container registry hostname. GHCR uses GITHUB_TOKEN auth automatically |\n| `target` | string | no | `\"devel\"` | Dockerfile target stage to publish. `devel` for app-base usage; `runtime` for production images |\n| `build_args` | string | no | `\"\"` | Multi-line KEY=VALUE build args (same shape as build-worker) |\n| `platforms` | string | no | `\"linux/amd64\"` | Comma-separated target platforms; multi-arch publishes a single multi-arch manifest under each tag |\n| `context_path` | string | no | `\".\"` | Build context (mirrors build-worker) |\n| `dockerfile_path` | string | no | `\"\"` | Optional explicit Dockerfile path |\n| `build_contexts` | string | no | `\"\"` | Optional newline-separated `\u003cname\u003e=\u003clocation\u003e` build contexts |\n| `test_tools_version` | string | no | `\"latest\"` | `ghcr.io/.../test-tools:\u003ctag\u003e` build-arg (pin to your template release for reproducibility) |\n\nCaller example (foundational multi-variant repo):\n\n```yaml\n# .github/workflows/main.yaml\njobs:\n  call-publish:\n    needs: ci-passed\n    if: startsWith(github.ref, 'refs/tags/')\n    permissions:\n      contents: read\n      packages: write\n    strategy:\n      matrix:\n        target:\n          - { name: 'standard',  base: 'python:3.11-slim',     is_latest: true }\n          - { name: 'minimal',   base: 'python:3.11-alpine',   is_latest: false }\n    uses: ycpss91255-docker/base/.github/workflows/publish-worker.yaml@vX.Y.Z\n    with:\n      image_name: my_image\n      tag_suffix: \"-${{ matrix.target.name }}\"\n      is_latest: ${{ matrix.target.is_latest }}\n      target: devel\n      build_args: |\n        BASE_IMAGE=${{ matrix.target.base }}\n```\n\nAfter a `v0.1.0` tag push, the matrix above yields:\n\n```\nghcr.io/\u003corg\u003e/my_image:v0.1.0-standard\nghcr.io/\u003corg\u003e/my_image:latest-standard   # is_latest = true\nghcr.io/\u003corg\u003e/my_image:v0.1.0-minimal\n```\n\nDownstream app repos then `FROM ghcr.io/\u003corg\u003e/my_image:v0.1.0-standard` in their own Dockerfile, dropping the duplicated sys / base / devel layers.\n\n## Running Template Tests\n\nUsing `Makefile.ci` (from template root):\n```bash\nmake -f Makefile.ci test        # Full CI (ShellCheck + Bats + Kcov) via docker compose\nmake -f Makefile.ci lint        # ShellCheck only\nmake -f Makefile.ci clean       # Remove coverage reports\njust                      # Show repo recipes\nmake -f Makefile.ci help  # Show CI targets\n```\n\nOr directly:\n```bash\n./script/ci/ci.sh          # Full CI via docker compose\n./script/ci/ci.sh --ci     # Run inside container (used by compose)\n```\n\n## Tests\n\nSee [TEST.md](doc/test/TEST.md) for details.\n\n## Directory Structure\n\n```\n.base/\n├── init.sh                           # Initialize repo (new or existing)\n├── upgrade.sh                        # Upgrade template subtree version\n├── script/\n│   ├── docker/                       # Docker operation scripts\n│   │   ├── wrapper/                  # User-facing wrapper scripts\n│   │   │   ├── build.sh\n│   │   │   ├── run.sh\n│   │   │   ├── exec.sh\n│   │   │   ├── stop.sh\n│   │   │   ├── prune.sh\n│   │   │   ├── setup.sh              # .env generator\n│   │   │   └── setup_tui.sh          # Interactive setup editor\n│   │   ├── lib/                      # Shared helper modules\n│   │   │   ├── _lib.sh               # Core wrappers library\n│   │   │   ├── bootstrap.sh          # Wrapper initialization\n│   │   │   ├── compose.sh            # Compose generation\n│   │   │   ├── conf.sh               # INI parser\n│   │   │   ├── conf_logging.sh       # Logging config\n│   │   │   ├── env.sh                # Environment setup\n│   │   │   ├── gitignore.sh          # Gitignore management\n│   │   │   ├── hook.sh               # Per-wrapper hooks\n│   │   │   ├── i18n.sh               # Language detection\n│   │   │   ├── log.sh                # Logging utilities\n│   │   │   ├── config_summary.sh     # Config summary\n│   │   │   ├── _tui_backend.sh       # TUI dialog/whiptail wrapper\n│   │   │   ├── _tui_conf.sh          # TUI INI validators\n│   │   │   ├── log-events.txt        # Log event catalog\n│   │   │   └── log.lnav-format.json  # Lnav format definition\n│   │   ├── runtime/                  # Runtime in-container scripts\n│   │   │   ├── entrypoint.sh         # Template entrypoint helper\n│   │   │   ├── logging.sh            # Host-side log tee helper\n│   │   │   └── smoke.sh              # Runtime install-check smoke\n│   │   ├── justfile                  # Docker operations entry (just)\n│   │   └── setup.conf                # Template runtime config defaults\n│   └── ci/                           # CI pipeline scripts\n│       ├── ci.sh                     # CI orchestration (local + remote)\n│       ├── lint_bare_stderr.sh       # Bare stderr lint checker\n│       └── lint_mixed_test_layout.sh # Mixed-tool test layout validator\n├── dockerfile/\n│   ├── Dockerfile.example            # Multi-stage template (sys / devel-base / devel / devel-test / [runtime-base / runtime / runtime-test])\n│   └── Dockerfile.test-tools         # Pre-built lint/test tools image\n├── config/                           # Container-internal shell/tool configs\n│   ├── docker/\n│   │   └── setup.conf                # Runtime config (per-repo override mirror: \u003crepo\u003e/config/docker/setup.conf)\n│   └── shell/\n│       ├── bashrc\n│       ├── bashrc.d/                 # Interactive shell bootstrap drop-ins\n│       │   └── .gitkeep\n│       ├── terminator/\n│       │   ├── setup.sh\n│       │   └── config\n│       └── tmux/\n│           ├── setup.sh\n│           ├── README.adoc\n│           └── tmux.conf\n├── test/\n│   ├── smoke/                        # Shared smoke tests + runtime assertion helpers\n│   │   ├── test_helper.bash          # assert_cmd_installed / _runs / file / dir / owned_by / pip_pkg\n│   │   ├── script_help.bats\n│   │   └── display_env.bats\n│   ├── unit/                         # Template self-tests (bats + kcov)\n│   │   ├── test_helper.bash\n│   │   ├── bashrc_spec.bats\n│   │   ├── build_sh_spec.bats\n│   │   ├── build_sh_prune_spec.bats\n│   │   ├── build_worker_yaml_spec.bats\n│   │   ├── ci_spec.bats\n│   │   ├── compose_gen_spec.bats\n│   │   ├── compose_logging_spec.bats\n│   │   ├── compose_overlay_spec.bats\n│   │   ├── conf_logging_spec.bats\n│   │   ├── deploy_spec.bats\n│   │   ├── entrypoint_logging_spec.bats\n│   │   ├── exec_sh_spec.bats\n│   │   ├── gitignore_spec.bats\n│   │   ├── hook_spec.bats\n│   │   ├── init_spec.bats\n│   │   ├── lib_spec.bats\n│   │   ├── lint_mixed_test_layout_spec.bats\n│   │   ├── log_spec.bats\n│   │   ├── makefile_user_spec.bats\n│   │   ├── multi_distro_build_worker_yaml_spec.bats\n│   │   ├── prune_sh_spec.bats\n│   │   ├── release_test_tools_yaml_spec.bats\n│   │   ├── run_sh_spec.bats\n│   │   ├── runtime_smoke_spec.bats\n│   │   ├── self_test_yaml_spec.bats\n│   │   ├── setup_spec.bats\n│   │   ├── smoke_helper_spec.bats\n│   │   ├── stop_sh_spec.bats\n│   │   ├── template_spec.bats\n│   │   ├── terminator_config_spec.bats\n│   │   ├── terminator_setup_spec.bats\n│   │   ├── tmux_conf_spec.bats\n│   │   ├── tmux_setup_spec.bats\n│   │   ├── tui_backend_spec.bats\n│   │   ├── tui_flow.bats\n│   │   ├── tui_mount_assembler_spec.bats\n│   │   ├── tui_spec.bats\n│   │   ├── upgrade_spec.bats\n│   │   └── wrapper_lib_lookup_spec.bats\n│   ├── integration/                  # Level-1 init.sh end-to-end tests\n│   │   ├── init_new_repo_spec.bats\n│   │   ├── upgrade_spec.bats\n│   │   ├── fresh_clone_portability_spec.bats\n│   │   ├── gitignore_sync_spec.bats\n│   │   └── wrapper_compose_dispatch_spec.bats\n│   └── behavioural/                  # Runtime integration tests\n│       └── runtime_test_smoke_spec.bats\n├── Makefile.ci                       # Template CI entry (make test/lint/...)\n├── compose.yaml                      # Docker CI runner\n├── .hadolint.yaml                    # Shared Hadolint rules\n├── .dockerignore\n├── codecov.yml\n├── .github/workflows/\n│   ├── self-test.yaml                # Template CI\n│   ├── build-worker.yaml             # Reusable build + smoke-test workflow\n│   ├── release-worker.yaml           # Reusable release (source archive) workflow\n│   ├── publish-worker.yaml           # Reusable image publish workflow (opt-in)\n│   ├── multi-distro-build-worker.yaml # Multi-distro build workflow\n│   └── release-test-tools.yaml       # Template's own test-tools image release\n├── doc/\n│   ├── readme/                       # README translations\n│   │   ├── README.zh-TW.md\n│   │   ├── README.zh-CN.md\n│   │   └── README.ja.md\n│   ├── adr/                          # Architecture Decision Records\n│   │   ├── 00000001-setup-conf-vs-compose.md\n│   │   ├── 00000002-no-latest-tag.md\n│   │   ├── 00000003-env-vs-workload-param-boundary.md\n│   │   └── 00000004-test-category-tool-subdir-layout.md\n│   ├── test/\n│   │   └── TEST.md                   # Test catalog and spec tables\n│   ├── changelog/\n│   │   └── CHANGELOG.md              # Release notes\n│   └── deprecations.md\n├── .gitignore\n├── LICENSE\n└── README.md\n```\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fycpss91255-docker%2Fbase","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fycpss91255-docker%2Fbase","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fycpss91255-docker%2Fbase/lists"}