Skip to main content
Use 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 core sitectl 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.
The migration command in this scaffold is deliberately fail-closed guidance for the plugin author: it exits nonzero, so the final Compose health wait cannot run and deploy cannot claim success. Replace it before releasing a derived plugin. A final Compose health wait proves container health only; it cannot prove that an application schema migration happened.

Template contract

Templates may declare the revision of their component defaults in .libops/template-contract.yaml:
The same single-line revision may instead live in .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 keep main small and declarative:
The helper is intentionally a registration helper, not a new runtime abstraction. App behavior still lives in the template repository, buildkit image, or app-specific plugin command where it belongs. It registers discovery, create, ingress, optional dev-mode, route, and healthcheck behavior; it does not invent application-specific verification or migration logic. Choose the application-source owner before defining image metadata:
  • 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.
Do not combine the two models for one application tree. A bundled tree plus a downstream Composer install creates two owners whose files and versions can silently overwrite each other. 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

Core sitectl image set writes docker-compose.override.yml for local contexts:
Image overrides affect reconcile:
  • 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
Known buildable application services reject --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 from sitectl-app-tmpl:
  1. Rename the Go module, binary, plugin name, display name, and release workflow outputs.
  2. Set TemplateRepo, a stable TemplateBranch, DefaultPath, AppService, AppImage, DatabaseService, DatabaseName, and codebase rootfs constants. Do not release a plugin whose create default still follows main.
  3. Update CreateSpec lifecycle commands, InitArtifacts, InitVolumes, and Images to match the real template repository. Replace the migration placeholder with supported fail-hard automation or a documented fail-closed manual-migration gate before release.
  4. Prefer RegisterStandardComposeAppPlugin; tune its Discovery option only when service-name detection is not enough. Keep the scaffold’s DisableDevMode: true default unless the derived plugin can declare downstream-owned paths or explicitly named custom extensions.
  5. Keep app-specific commands in the plugin namespace. Avoid registering build, init, up, down, status, logs, or rollout unless the app has a deliberate compatibility need.
  6. Use core service commands for shared operations. For example, document sitectl mariadb backup app rather than adding a thin app plugin alias.
  7. 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.
  8. Keep local workspace wiring in go.work; do not add sibling-module replace directives to go.mod.
  9. Update the integration script to create with --setup-only, start with sitectl compose up, and then run both sitectl healthcheck and sitectl verify.
  10. Configure the isolated Homebrew tap-reconciliation job for a tap the derived repository controls and provide HOMEBREW_REPO only to that job, or disable tap reconciliation. Keep GoReleaser on the source repository’s scoped GITHUB_TOKEN; do not expose the tap token to it. Leave publish-package-repo: false unless the repository also has its own trusted package publisher, cloud identity, and required variables.
  11. 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.
  12. Add a plugin page to sitectl-docs for app-specific operations.
  13. 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 REQUIRED gate, keep ingress stopped, and document the manual migration, full-stack start, and post-migration checks.

Local Verification

Run the standard local workflow:
Then verify the installed plugin can be discovered and used through core commands: