sitectl-app-tmpl v1.0.0 when adding a new application plugin for a standalone template repository. Record the scaffold release and exact tag commit in the initial repository history so later maintainers can distinguish inherited behavior from application-specific changes.
The template is intentionally thin. The application repository owns the Compose workload. Core sitectl owns shared lifecycle commands. The plugin contributes app-specific metadata and helpers.
Command Ownership
New app plugins should usually call
RegisterStandardComposeAppPlugin. That helper registers the common compose-backed plugin shape in one place: project discovery, create metadata, ingress component wiring, optional dev-mode wiring, healthcheck registration, and public route discovery.
Use the lower-level RegisterComposeTemplateCreateRunner only when an app plugin needs custom registration behavior. Avoid RegisterStandardComposeTemplate for first-class app plugins unless the plugin deliberately wants direct namespace lifecycle commands; core sitectl compose should provide the shared lifecycle surface.
Create Definition Contract
The create definition is more than a clone recipe. It is also the desired state metadata used by coresitectl compose up to decide whether a local checkout needs first-run init or image build work. See Compose reconcile contract for the shared core behavior.
A released plugin must point DockerComposeBranch at a reviewed stable template
ref. The current LibOps application plugins use v1.0.0; main is for scaffold
development, not a durable downstream create default. Advance the template ref only
after its clean-create, migration, health, and verification smoke tests pass.
Template contract
Templates may declare the revision of their component defaults in.libops/template-contract.yaml:
.libops/component-defaults.revision; when both are present they must match. Use a
new stable revision whenever a template changes the default fields that a component
will later detect or converge. The source template must never contain
.libops/template.lock.yaml; core sitectl writes that downstream-owned provenance
file after it resolves the exact source commit.
The contract does not authorize a plugin to replace downstream customizations. A
component release still needs explicit field ownership, transition logic from each
supported defaults revision, and tests proving that unrelated downstream content is
preserved.
Standard App Registration
Most application plugins should keepmain small and declarative:
- For a release-bundled application, Buildkit downloads and verifies the upstream release and publishes an application-versioned image. The downstream Dockerfile extends that image and adds only tracked customizations. OJS, Omeka, and ArchivesSpace use this model.
- For a Composer-owned application, the downstream checkout keeps
composer.json,composer.lock, and application/custom code. The image supplies the runtime, and the downstream build runs Composer. Drupal, ISLE, and WordPress use this model.
DockerComposeInit should be idempotent. It may run an init service and write deterministic secret files. It must not destroy existing application data when run again.
Use a dedicated database-init service when an application database or scoped user must be created. Among non-database workloads, only that one-shot service should receive DB_ROOT_PASSWORD; the application receives the scoped password. The application must require database-init with condition: service_completed_successfully and MariaDB with condition: service_healthy. Keep the initializer script generic and idempotent so retries do not rotate or destroy data.
DockerComposeRollout should keep rollout commands in plugin metadata, not in a template scripts/rollout.sh or Makefile target. Core rollout/deploy handles git sync from the checkout’s configured upstream branch when one exists, then runs the plugin’s Compose rollout commands.
Put standalone Compose pull/build commands at the beginning of DockerComposeRollout. Core v1 runs that contiguous preparation prefix while the current stack is still online and stops before the first non-pull/build command. Keep each preparation operation in its own metadata entry; a compound build-and-up command remains inside the outage window because it is not safe to classify as preparation.
Keep rollout entries as simple top-level docker compose commands or shell lists of those commands. Core v1 applies the context’s selected Compose files and environment files to every recognized branch, including remote rollouts, and preserves the fallback pull pattern shown above. Ambiguous shell grammar such as redirections, subshells, functions, and background commands is deliberately left unchanged; do not hide lifecycle behavior behind it.
For an application with an automatic schema migration, the first post-down compose up should target only the application service in detached mode. Its required database and one-shot dependencies start through the tracked Compose graph, while ingress and unrelated workers remain stopped. Follow it with a bounded application-readiness check, the fail-hard migration, and a bounded full-stack compose up --wait.
Applications without a supported noninteractive migration must instead fail at an unambiguous ACTION REQUIRED gate, leave public ingress stopped, and document that deploy cannot attest to completion. A printed notice by itself is not a gate. The runbook must tell the operator how to complete the supported web migration, start the full stack, and then run health and application verification before restoring traffic.
InitArtifacts should list files that prove init has completed. Prefer explicit files over probing container state. Use ValueFrom: plugin.InitArtifactValueFromHostUID for a UID marker file when generated files need to match the local host user.
InitVolumes should list named Compose volumes that prove first-start runtime state exists, such as database volumes and application file/upload volumes. Core resolves those names through docker compose config, so custom Compose project names and explicit volume names still work.
Images should list local images the template expects to build. Use BuildPolicyIfNotPresent when Compose gives the derived image a distinct explicit image: name that core can inspect. Use BuildPolicyAlways when the application service has build: but no distinct output image name; it bypasses the reconcile cache so edits in the downstream build context are seen immediately, while BuildKit reuses unchanged layers. Use BuildPolicyNever for services that should never trigger the build phase.
Pin every directly consumed Compose image as tag@sha256:digest. Keep a versioned tag for reviewability and the digest for reproducibility. A buildable application’s BASE_IMAGE remains a build argument so local --tag and explicit tag@digest testing work without deleting the Compose build contract.
Register dev-mode only when the downstream checkout owns every mounted tree, or when each mount targets one explicitly named custom extension. Never mount a broad plugins, modules, themes, locales, or stylesheets directory over content bundled in a versioned base image. The scaffold sets DisableDevMode: true; keep that default until a derived plugin can replace it with a safe, explicit DevModeOptions mount list.
The standard core healthcheck accepts a stopped service only when the effective Compose model proves it is a required service_completed_successfully dependency and its container is exactly exited (0). Do not rely on that exception for optional jobs or ad hoc docker compose run containers.
Image Overrides
Coresitectl image set writes docker-compose.override.yml for local contexts:
- an explicit image override for a non-buildable service means core does not require the plugin’s default image to exist locally
- a build-arg override triggers the build phase so the new arguments are applied
- the override file fingerprint is part of the reconcile cache key
--image; use a known --tag or --build-arg SERVICE.BASE_IMAGE=... so Compose still produces the downstream image. Reserve --image for non-buildable dependency services.
Template Checklist
After creating a repository fromsitectl-app-tmpl:
- Rename the Go module, binary, plugin name, display name, and release workflow outputs.
- Set
TemplateRepo, a stableTemplateBranch,DefaultPath,AppService,AppImage,DatabaseService,DatabaseName, and codebase rootfs constants. Do not release a plugin whose create default still followsmain. - Update
CreateSpeclifecycle commands,InitArtifacts,InitVolumes, andImagesto match the real template repository. Replace the migration placeholder with supported fail-hard automation or a documented fail-closed manual-migration gate before release. - Prefer
RegisterStandardComposeAppPlugin; tune itsDiscoveryoption only when service-name detection is not enough. Keep the scaffold’sDisableDevMode: truedefault unless the derived plugin can declare downstream-owned paths or explicitly named custom extensions. - Keep app-specific commands in the plugin namespace. Avoid registering
build,init,up,down,status,logs, orrolloutunless the app has a deliberate compatibility need. - Use core service commands for shared operations. For example, document
sitectl mariadb backup apprather than adding a thin app plugin alias. - Implement debug, validate, healthcheck, and verify runners through the SDK. The template’s verify runner deliberately returns an unimplemented warning; replace it with behavioral checks before releasing a derived plugin. Release smoke tests must run
sitectl verify --strict, which fails for missing, empty, or warning-only verification. Results should be structured; progress and diagnostics should go to stderr. - Keep local workspace wiring in
go.work; do not add sibling-modulereplacedirectives togo.mod. - Update the integration script to create with
--setup-only, start withsitectl compose up, and then run bothsitectl healthcheckandsitectl verify. - Configure the isolated Homebrew tap-reconciliation job for a tap the derived repository controls and provide
HOMEBREW_REPOonly to that job, or disable tap reconciliation. Keep GoReleaser on the source repository’s scopedGITHUB_TOKEN; do not expose the tap token to it. Leavepublish-package-repo: falseunless the repository also has its own trusted package publisher, cloud identity, and required variables. - Add a versioned template contract/component-defaults revision, confirm the source does not contain
.libops/template.lock.yaml, and test that local and remote create record the exact template commit. - Add a plugin page to
sitectl-docsfor app-specific operations. - Test a clean create and an upgrade rollout. Keep the pull/build preparation prefix before the outage boundary. Start only the application and its required dependencies before an automatic migration, make migration commands fail hard, bound every readiness wait, and finish with a bounded full-stack wait. Applications without a supported CLI migration must fail at an
ACTION REQUIREDgate, keep ingress stopped, and document the manual migration, full-stack start, and post-migration checks.

