Skip to main content
sitectl application templates are normal projects with documented project rules. The repository owns the application workload, and sitectl supplies context-aware lifecycle commands, component updates, health checks, image overrides, and service helpers. These template and core/plugin workflows are standalone: they do not require a LibOps platform account or the optional sitectl-libops plugin.

Compose Files

docker-compose.yml or docker-compose.yaml is the production contract. It should describe the stack that runs on the host: services, volumes, secrets, Traefik ingress, health checks, build definitions, and the default published ports. Local-only changes can live in docker-compose.override.yml. Templates may ignore that file for private workstation experiments, or a team can track a named file such as docker-compose.dev.yml and select it only in local contexts. Set the context’s Compose files in merge order—for example, docker-compose.yml followed by docker-compose.dev.yml—and every sitectl compose, deploy, health, and validation operation uses the same effective project. Contexts for the same site can therefore share the canonical stack while selecting different tracked overlays. Production should only select files that describe reviewed production behavior. When an application service requires MariaDB during startup, declare that relationship in the tracked Compose file with depends_on and condition: service_healthy. The application plugins’ standard healthcheck runner verifies that policy for MariaDB-backed web apps. Database provisioning is a separate one-shot boundary. The tracked database-init service receives the MariaDB root secret, creates the application database and scoped account, and exits. The application receives only its scoped database password and declares database-init with condition: service_completed_successfully. Core healthchecks accept an exited container only when the effective Compose model proves that another service requires it with that condition and it exited with status 0. An arbitrary, optional, failed, or dead job is still unhealthy.

Local Image Builds

Every application template includes a Dockerfile for the downstream application image, but the application source has two ownership models: For the first family, the upstream application version is part of the Buildkit image tag (with a runtime suffix where applicable) and the downstream Dockerfile extends that versioned image. For Composer-owned stacks, the Buildkit tag selects the runtime rather than the application dependency set; do not copy a second application tree into that image. Update the checked-in Composer files and rebuild the downstream image. Runtime configuration stays in the Compose environment where possible. This keeps local builds fast:
  • Docker can reuse the published application/base layer and package-manager cache layers when only local custom code changed.
  • The build uses the platform selected by the Docker CLI on the host.
  • Local builds do not push images. Publishing images belongs in CI or another explicit release workflow.
The LibOps base images provide the default PHP and nginx configuration. When a template needs to tune upload size, timeouts, trusted proxy handling, or similar runtime behavior, prefer environment values in the Compose service over rebuilding nginx or PHP config into the downstream template image. For PHP/nginx application services, the shared ingress component propagates the reviewed values through Traefik, nginx, PHP, and PHP-FPM. See the exact ingress runtime tuning matrix, including accepted units, defaults, and the ArchivesSpace exception. A plugin for another runtime must map the component value to that application’s supported backend setting before describing it as end to end.

Runtime Configuration Boundary

LibOps images use s6-overlay to order long-running services and one-time setup, while confd renders nginx, PHP-FPM, application, and other dependent-service configuration from the container environment and mounted secrets. The tracked Compose environment is the source of truth. Do not edit rendered files inside a container or bind-mount a broad replacement over them; the next start can regenerate them. Change supported settings through template environment values or a sitectl component. This includes upload limits, request/read timeouts, app URLs, proxy trust, and service endpoints. Keep passwords and keys in Compose secrets and use the image’s documented secret-file variables rather than putting secret values directly in environment fields. Use sitectl image set when you need a local context to run a different image, tag, or build argument without editing the tracked template files.

Image Versions and Digests

Tracked Compose files pin directly consumed images with both a readable tag and an immutable digest, for example libops/mariadb:11@sha256:.... Keep both. The tag communicates the intended release line; the digest makes a checkout reproducible and prevents an unrelated registry update from changing the deployed bytes. Prefer a semantic release tag when the publisher provides one. A moving label such as main is still byte-stable only when the digest remains present, but it communicates less release intent to reviewers. The downstream Dockerfile’s BASE_IMAGE is intentionally selected by a versioned application or runtime tag so sitectl image set --tag can switch it locally. For a reproducible local test, use sitectl image set --build-arg SERVICE.BASE_IMAGE=tag@sha256:digest. To promote those exact base bytes, put the tag@sha256:digest default in the tracked Dockerfile or main Compose build arguments and commit it; the ignored local override is not a production lock. sitectl deploy builds with --pull, so an unpinned base tag can otherwise resolve to newer bytes. Use sitectl bump or a reviewed Renovate pull request to update tracked tags and digests together. Application-version changes also require the application’s backup and migration procedure; an image update being available is not evidence that its schema migration succeeded.

Updating a Downstream Fork

Treat template updates like application changes, not generated files to overwrite:
  1. Create an update branch with a clean working tree and current database/file-volume backups. Read .libops/template.lock.yaml to identify the exact template repository and commit from which the site was created.
  2. Compare that recorded commit with the new reviewed template tag, then merge or apply the upstream change using normal Git workflows. Resolve changes in files your fork owns before running sitectl. The lock is a provenance record, not an instruction for sitectl to overwrite the fork.
  3. Run sitectl validate, then sitectl converge --report. Converge compares component-owned paths with the versioned intent in .libops/site.yaml, including each selected disposition and setting, and reports only the changes needed to restore that intent.
  4. Apply sitectl converge or an explicit sitectl set when needed, inspect the Git diff, and commit the resulting component files with the upstream update.
  5. Retain the create-time lock as the site’s origin record. Record the applied upstream template tag and commit in the update commit or pull request; do not hand-edit the lock to imply that a fresh create occurred.
  6. Build and test the branch, then use sitectl deploy. Complete every automatic or manual application migration gate before declaring the update finished.
Keep app-specific customizations outside component-owned blocks when possible. When a feature must extend a managed component, update the plugin/component contract as well as the template so a later converge can preserve that customization deliberately.

Development Ports

Application templates should publish standard ingress ports in the tracked Compose file:
If the stack serves HTTPS directly, publish 443:443 in the tracked Compose file as well. sitectl infers HTTPS for local URL detection when the Compose project publishes target port 443 or Traefik declares an HTTPS entrypoint on :443; otherwise it uses HTTP. For local development contexts, sitectl compose up checks whether 80 or 443 is already occupied. If another process owns a needed port, it writes docker-compose.override.yml with a Compose ports: !override entry such as:
That smart port allocation is a development convenience only. It should not be committed and should not be part of production deployment.

Local HTTPS

If production runs HTTPS and a developer wants the local site to behave the same way, use the ingress component:
See Components for ingress settings and Compose commands for the local port behavior.

Assistant Dev Mode

Plugins register dev-mode only when the downstream checkout owns the mounted dependency tree or can declare narrow bind mounts that do not hide application code from the image. Drupal, WordPress, and ISLE currently provide that contract. OJS, Omeka, and ArchivesSpace intentionally do not expose broad bundled-extension directory mounts. For a plugin that supports it, sitectl set dev-mode --assistant enables the development bind mounts and adds a profiled cli-sandbox service to docker-compose.override.yml.
The application plugin owns the editable code mount list. Drupal and WordPress mount Composer-owned downstream trees; WordPress includes all three Bedrock installer destinations (mu-plugins, plugins, and themes). ISLE mounts explicit custom-code and configuration paths. For applications whose versioned base image bundles core plugins, modules, or themes, downstream customization should be added through the tracked build context or an explicit per-extension mount and rebuilt instead of masking the whole bundled directory at runtime. Sitectl selects the interactive egress profile by default because this command is normally run by a trusted developer. Interactive mode allows live DNS and broader provider networks, so it is not a confidentiality boundary and must not be used for an untrusted platform task. The LibOps platform controller selects --egress-profile managed and supplies the per-project Private Service Connect model-gateway URL as a literal IPv4 http:// or https:// URL. In that profile the sandbox blocks DNS and IPv6 and allows TCP only to the gateway’s exact /32 and port. The URL is an operator-controlled input, not a task-controlled setting. Use --compose-access only when a trusted interactive coding agent should reach the Compose network and host Docker daemon. That adds the Docker socket mount and socket group to the cli-sandbox service. The egress firewall remains enabled by default and receives NET_ADMIN/NET_RAW; --skip-egress-firewall sets SKIP_EGRESS_FIREWALL=true and omits those capabilities. Both options are interactive-only escape hatches and must never be enabled for the managed Task Agent.