Skip to main content
Version: 1.3

Environments

An environment composes templates into an application stack — postgres + backend + frontend. It belongs to a project and is a pure definition: creating one starts nothing. What runs is an instance of it.

Environment services​

Adding a template to an environment creates an environment service. Its name is unique within the environment and plays exactly the role a service key plays in a docker-compose.yml: it is what siblings resolve over the network, and it prefixes the deployed container names. It defaults to the template's name, so the same template can be added twice under different names.

The name is a lowercase slug — a letter or digit, then letters, digits and dashes, up to 40 characters. That is not a house style: the name is concatenated into container names, volume names and the ingress router that publishes the service, so web-api works and Web API or web.api cannot. Leave the field empty and the template's own name is used, which is already in that shape.

Each environment service carries:

  • An image tag — the version this environment runs. See versions below.
  • Environment variable overrides — applied on top of the template's own values.
  • Injected files — see files below.
  • A start wave — an integer, 0 by default.
  • A job flag — for run-once work like a migration.

The row also says where the service came from: its template name is a link, opening that template on the project's templates page, and a template imported from a catalog carries a catalog badge linking to the entry it was copied from. Provenance is one click away from the environment rather than something to go looking for — and the badge stays honest when the entry is gone from the catalog.

Start order: waves​

Services with the same wave number start together; a wave does not begin until the previous one is ready. "Ready" means healthy for services that declare a healthcheck and started for those that do not — which is the practical reason to give databases a healthcheck.

wave 0 postgres, redis ← start together
wave 1 migrate (job) ← waits for wave 0 to be healthy
wave 2 api, web ← waits for the migration to exit 0

Wave 0 means "no ordering", so a stack with no waves configured simply starts everything at once. The whole readiness wait is bounded by fadebox.deploy.ready-timeout (default 5 minutes).

Jobs​

Marking an environment service as a job changes four things: restart is forced off regardless of what the template says, the next wave gates on the container completing successfully, readiness counts it done only once it has exited 0, and it re-runs on every deploy. Jobs must therefore be idempotent — a migration that fails when re-applied will fail the second deploy.

Versions live on the environment​

Each environment service may set an image tag — "this environment runs api:1.4.0". Left empty, the template's own tag applies. The tag replaces the tag only; the repository stays as the template defined it, and the retag follows the image, not the container: helpers built from the main service's repository (a migration container running the app image) get the same tag, while a helper on a foreign image keeps its own.

The tag can also be a git reference ({{git:app-repo:deploy/values.yaml#$.image.tag}}), resolved fresh on every deploy — the GitOps values file stays the single source of truth and an ordinary release needs no write to Fadebox.

Because versions are part of the definition, every instance of an environment runs the same versions by default. "The same stack at other versions" is another environment — see cloning below — unless you deliberately hand the choice to whoever deploys, which is what open services are for.

What deployers may change​

An environment normally decides everything: an instance is a name and a runtime, and what it runs is the definition. That is right when the people making instances are the people who wrote the definition, and wrong as soon as they are not — a tester who needs 1.3.2 to reproduce a bug, a trainer who wants one seed dataset per attendee, a support engineer chasing a customer's data. Handing them editor so they can clone is the wrong answer: it hands them the whole definition, credentials included.

Instead, an environment names the services a deployer may vary, and in what. Each service row carries two switches, both off:

SwitchWhat a deployer may then set on that service
VariablesEnvironment variables on the service's containers.
Image tagThe service's image tag — the repository stays as the template fixed it.

In the UI they are the Deployers may change control on the service row, editable by an editor and shown locked on a protected environment as every other composing write is. fadebox env service set and the assistant's set_service_access do the same thing.

Nothing else is delegable, and that is the boundary the feature rests on: a deployer sets values, never structure. Mounts, ports, networks, labels, privileged and user live in the template, out of reach. An environment that opens nothing behaves exactly as it always has.

The switches are part of the definition, so cloning copies them and a blueprint keeps them. Exporting to Compose ignores them — they are not a Compose concept. What a deployer then does with them is on the instance.

Where environment variables come from​

Three layers, merged at deploy time. Later layers win:

  1. Environment shared variables — key/value pairs on the environment itself. These sit underneath everything, so they fill in rather than overrule: anything a template already set keeps its value. Uniquely, they reach every container, helpers included — an init container usually needs the same connection settings as the service it prepares.
  2. The template's own environment:.
  3. The environment service's overrides — main service only, the last word.

Values may also be instance placeholders, references into a git repository, or references to sibling services — all resolved at deploy time, in that order. Service references resolve last, so a consumer always receives the provider's final value: a database password the provider takes from git reaches every service that references it already resolved. The three families, and which surfaces take which, are covered together in Placeholders.

Injected files​

An environment service can carry files written into its containers at deploy time, exactly like a template's own files: each names a container inside the unit, an absolute target path, and its content. One with the same target path as a template file replaces it — how an environment swaps in its own config file without editing the shared template. Because they are keyed by container, they reach a unit's helpers too, not just the main service.

Shared or personal instances​

An environment also decides whom its instances belong to. New instances are shared, a switch on the environment, is what a new instance starts as. On, every deployer in the project may operate it. Off, it is personal to whoever created it: only they and the project's editors may deploy, stop, reset or delete it, while everyone else still sees it. A new environment starts with the switch off; every environment that existed before this feature has it on, so nothing changed on upgrade. Cloning copies it. The switch sets only the default: the person creating an instance can choose the other on the form, and the creator or an editor can flip it afterwards.

Turn it on for the environments a team shares — the one CI deploys per pull request, staging — and leave it off where each copy is somebody's own: a tester's, an attendee's, a demo.

Instance lifetime​

Instance lifetime in the environment editor sets how long this environment's instances run after their last deploy. It takes humane values — 7d, 12h, 90m. Empty inherits the installation default of seven days; 0 means never expire. Over the API the field is ttlSeconds on the environment update call, an integer number of seconds, where null inherits and 0 is never.

A change applies from the next deploy or extend — instances already running keep the expiry they were stamped with. Cloning an environment copies its lifetime along with the rest of the definition. What happens when the time runs out is an instance matter: the instance is stopped, never deleted.

Cloning​

Cloning copies an environment's definition under a new slug — services, image tags, environment-variable overrides, waves, injected files and shared variables. Instances are never copied; they stay with the original, and neither the blueprint flag nor protection is inherited.

This is how a second version stream is made: clone dev to dev-v2, then edit the tags. The variation is named and visible in the environment list, instead of hidden in per-instance parameters. In the dialog, typing a name suggests the slug; from a pipeline it is one call — fadebox env clone dev dev-v2.

Blueprints​

An environment can be marked a blueprint: a definition kept to be cloned from and never deployed. Creating an instance of one is refused, so a blueprint is the environment you can improve freely, knowing nothing is running from it.

The problem it solves shows up once a team clones regularly. The environment everyone copies — call it base — is an ordinary environment, so it can also be deployed, and over time it collects instances of its own. Now editing it to improve the next clone risks disturbing something live, and the "canonical" environment is only canonical by convention. Marking it a blueprint makes that convention a rule.

Three things follow from it, and the third is the one worth remembering:

  • Instances are refused. The New instance button is gone in the UI; over the API and the CLI the call answers 409 with "… is a blueprint — clone it to deploy".
  • The flag can only be set while the environment has no instances, because that is exactly what it promises. If it still has some, the save is refused and tells you how many; delete them first. Clearing the flag has no such condition — a blueprint you decide you want to run just becomes an ordinary environment again.
  • Clones are deployable. A clone does not inherit the flag, which is the whole point: cloning is how a blueprint gets used. fadebox env clone base dev gives you a dev you can deploy immediately. When you genuinely want a second base — forking base into base-eu — the clone dialog offers a checkbox to make the copy a blueprint too, which is safer than cloning and then editing, since it leaves no moment where the copy is deployable.

Set it with the Blueprint checkbox in the environment editor, or blueprint: true on the create, update and clone API calls. Blueprints carry a blueprint badge in the environment list and on the environment page, and a blueprint value in the KIND column of fadebox env list.

Everything else about a blueprint is an ordinary environment: it holds services, image tags, shared variables and files, it can be exported as Compose like any other, and anyone who could edit an ordinary environment can edit it. A base the whole team clones from usually wants protection as well; the two flags answer different questions.

Protected environments​

An environment can be marked protected: its definition may only be changed by a project maintainer, even where composing is otherwise an editor right. It is the guard for the stack a team shares. The environment everyone deploys against — staging, uat — is the one where a retag somebody makes for their own reasons surprises three colleagues on their next deploy, and protection is how that stack stays in a few hands without keeping everyone else out of the composer.

Four things follow:

  • An editor's writes are refused. Services, waves, image tags, shared variables, injected files, and deleting the environment: the call answers 403 with the code environment_protected and a message saying to ask a maintainer. In the UI the composer's controls stay visible but disabled, carrying the same sentence.
  • Cloning is not refused. An editor may clone a protected environment and the clone is unprotected — copy staging, retag the copy, deploy it. Protection guards the original; it does not withhold what is in it.
  • Instances are untouched. Creating, deploying, stopping, extending and deleting instances of a protected environment stay a deployer right. Protection says who may change what the environment is, never whether it may run — demo is exactly the environment a product owner should deploy and must not edit.
  • Only a maintainer raises or lowers it, in either direction and with no precondition. Unlike the blueprint flag it makes no claim about what is running, so an environment with instances can be protected exactly as it stands.

Set it from the environment's settings, or with PUT and DELETE on /api/projects/{project}/environments/{slug}/protection. It is deliberately not a field on the environment update call, so an ordinary edit cannot flip it by accident. Protected environments carry a lock badge beside the name in the environment list and on the environment page.

Protection and the blueprint flag are separate questions — who may edit this? and can this be deployed? — and all four combinations mean something: a protected blueprint is the team's curated base, a protected deployable is staging, an unprotected blueprint is one person's private base, and an unprotected deployable is one person's own stack.

Exporting​

An environment can be exported as a standalone Docker Compose project, with the naming and ordering fadebox would apply: plain YAML when it has no injected files, otherwise a zip of docker-compose.yaml plus the bind-mounted files/. Useful for review, and for running the same stack outside fadebox.

Service references are resolved in the export: the environment's definition holds everything they need, so {{service.db.host}} comes out as db and {{service.db.env.POSTGRES_PASSWORD}} as the actual value — the exported file runs. What has no resolver outside fadebox — {{git:…}} image tags, instance placeholders, and a service reference whose provider is missing or whose value is itself such a placeholder — comes out verbatim, under a header comment saying to replace it with a literal before running the file, never silently substituted with something the environment didn't say. Output stays deterministic either way, which is what makes diffing an export against a committed golden file a usable drift check.