Skip to main content
Version: Next

Service templates

A service template is one service plus its helpers, written as a Docker Compose document. Templates are the reusable building blocks: an environment is assembled by wiring templates together, and nothing runs until an instance of that environment is deployed.

A template provides exactly one service​

A template's compose document may contain several containers, but exactly one of them is the main service — the thing the template provides, and the one whose name the template contributes to an environment. Every other container must declare what it is, with the fadebox.role label:

fadebox.roleMeaning
(absent, or main)The main service. Exactly one per template.
initA run-once helper. Restart is forced off, the next wave waits for it to complete, and readiness counts it done only when it exits 0.
sidecarA long-running companion — a log shipper, a proxy, a metrics exporter.

Saving a template with zero or several role-less services is rejected with a message naming them. The rule exists so that reading the YAML always tells you what each container is for, and so that "add this template to my environment" adds one predictable service name rather than an unknown number of them.

Two peer services — an API and a database — are two templates composed in an environment, not one template with two role-less services.

services:
app: # main: no fadebox.role label
image: registry.example.com/shop/api:1.4.0
labels:
fadebox.ingress.port: "8080"
depends_on:
migrate:
condition: service_completed_successfully

migrate:
image: registry.example.com/shop/api:1.4.0
command: ["./migrate.sh"]
labels:
fadebox.role: init

Naming inside an instance​

When an environment service built from this template is deployed, containers are named {namespace}-{serviceName} for the main service and {namespace}-{serviceName}-{helper} for its helpers, where the namespace is {project}-{environment}-{instance}.

Within the instance, services reach each other by their environment service name — the same way they would in a plain compose file. A template referencing postgres:5432 works as long as the environment has a service called postgres.

Scope: global or project​

A template is either global (visible to every project) or scoped to one project. Names are unique within a scope, so shop/api and billing/api can coexist, and a project may keep its own postgres alongside the global one. The environment composer lists global templates plus the project's own.

Environments reference templates by identity, not by name, so scoping only affects what you can see and pick — it never changes how a deployed instance resolves anything.

The catalog​

Ready-made templates ship inside the application and are browsable under Catalog with no network access required — databases (PostgreSQL, MySQL, MariaDB, SQL Server, MongoDB, Redis), messaging (RabbitMQ, Kafka), Keycloak (standalone or PostgreSQL-backed), search and storage (OpenSearch, Elasticsearch, RustFS), cloud emulators (LocalStack, Azurite) and dev tools (WireMock, pgAdmin, Mailpit and a whoami demo). Each card carries two versions: the entry's own (v1.0.0, its revision in the catalog, the update signal) and the tag of the image its main service runs (18-alpine, 26.7) — what you compare when choosing between two entries for the same software; hover it for the full image reference.

Entries come from sources. The bundled catalog is one; beside it you can register git repositories serving the same format — the official catalog published as a repository, or an internal catalog of your own team's building blocks, for the whole installation or for one project. Any source can be switched off, the bundled one included. See Catalog sources.

Details on a card opens the entry itself: the Compose it deploys and the files it injects with their service and target path, highlighted like the editors, and its version, image, tags and pairing. Read-only, and worth reading before importing — until now the only way to see an entry's contents was to import a copy you might not want, which in a project scope you may not be allowed to create. The sheet has its own address (/catalog?entry=bundled/postgres), so it can be linked to and shared.

Importing an entry copies it into an ordinary, editable template — globally or into one project, your choice at import time. There is no live coupling: the copy records which source, entry and version it came from, and nothing about it ever changes on its own.

A copy's catalog badge on the Templates page links back to the entry it came from, in that template's own scope, and names the source, entry and imported version on hover. The entry may be gone by then — dropped from the catalog, or its source removed or switched off — and the sheet says which rather than failing: an import is a copy, so the template keeps working either way.

When a source later carries a newer version of that entry, the copy's row on the Templates page shows an update hint (“v1.1.0 available”), and Update from catalog re-imports the entry in place: the template's spec, files and description follow the catalog, its name and slug stay yours. It is explicit and confirmed — local edits to the copy are overwritten, and nothing ever updates automatically.

The copy's name and slug are yours to set in the import dialog; they default to the entry's. Since template names are unique per scope, renaming is what lets one entry land twice in the same scope — import PostgreSQL as postgres and again as postgres-analytics, tune each copy separately. Provenance still records the entry either way, so a renamed copy remains recognisable as a copy of it.

Well-known endpoints​

Catalog entries are built to connect to each other by convention. Inside an instance every container answers to its service name, so as long as you add a provider under its default name, any entry (or template of your own) can reach it at the address below — with the throwaway development credentials each entry ships:

EntryAddressCredentials
PostgreSQLpostgres:5432postgres / postgres, database postgres
MySQLmysql:3306root / root — or mysql / mysql, database app
MariaDBmariadb:3306mariadb / mariadb (root: mariadb), database mariadb
SQL Servermssql:1433sa / Passw0rd!
MongoDBmongodb:27017mongo / mongo
Redisredis:6379no auth
RabbitMQrabbitmq:5672guest / guest
Kafkakafka:9092plaintext, no auth
Keycloakkeycloak:8080admin admin / admin
OpenSearchopensearch:9200security disabled
Elasticsearchelasticsearch:9200security disabled
RustFSrustfs:9000 (S3 API)rustfsadmin / rustfsadmin
LocalStacklocalstack:4566any AWS key pair
Azuriteazurite:10000/10001/10002standard devstoreaccount1 account
WireMockwiremock:8080—
Mailpitmailpit:1025 (SMTP)no auth

Consumer entries carry service references for these connections — Keycloak (PostgreSQL) reaches its database as {{service.postgres.host}} and reads its credentials from the postgres service — so a missing provider fails the deploy with a message naming both sides, and overriding the provider's credentials rewires every consumer. Such entries declare their pairing: the catalog card shows a pairs with badge (the first provider and a count when there are several; hover for all of them), and the import dialog says whether the scope already holds a copy of the provider — recognised by import provenance, so a renamed copy counts — or asks you to import it too. Importing it stays your move: nothing is ever imported implicitly. Add both to the environment, the provider in an earlier wave. Renaming a service on add (or importing an entry under another slug) moves it off its well-known address; override the consumer's references in the environment to point at the new name. Kafka is the strictest case: its advertised listener hardcodes kafka, so keep it under that name.

These are development defaults, stated in each entry's description on purpose — override them at the environment level for anything shared or long-lived.

Next​