BlogGetting started with Fadebox, part 2 of 2

Your first environment: PetClinic on Fadebox

A database from the catalog, a template you write for a real app, an environment composing both, and an instance on a URL. Plus: templates versus the catalog.

The previous post left Fadebox running on your machine with a runtime that routes by name and a project called demo. This one builds on it. By the end, Spring PetClinic, the sample application every Java developer has seen at least once, is running against PostgreSQL in an instance you created, at a URL you can open. Then a second one beside it.

Everything here happens inside the project, with a project role. Nothing touches a global setting. If you skipped the first post: you need Fadebox installed, a runtime with the ingress domain localtest.me, and a project demo in which you are a maintainer.

Templates and the catalog are different things

Fadebox has two nouns that new users blur together, and the distinction is worth two minutes because everything else rests on it.

A template is a service described as Docker Compose YAML: one main container plus, optionally, its helpers, with a healthcheck, volumes and the files Fadebox injects at deploy time. It lives in your Fadebox, in the database, and it is yours: you edit it, you version it, you decide who sees it. A template is either global, visible to every project, or scoped to one project, where that project’s maintainers own it. Infrastructure blocks tend to be global. Your own application’s template belongs with its project.

The catalog is where ready-made templates come from. It is a browse-and-import surface, not a place anything runs from. Fadebox ships with a bundled catalog of curated entries, PostgreSQL, Redis, Keycloak, Kafka, RustFS, Mailpit and some twenty more, baked into the image so it works offline. Beside it you can register your own catalog sources: git repositories in the same format, which is how a platform team publishes the company’s building blocks.

The relationship between the two is copy-on-import. Importing an entry snapshots it into an ordinary, editable template under a name and scope you choose. There is no live link. A catalog that changes, breaks or disappears can never reach a template you imported, let alone a running instance. What the copy keeps is provenance: which source, which entry, which version. When the catalog moves on, the template shows an update available badge, and updating it is an explicit act that you confirm, never something that happens to you.

So: a template is the thing you compose environments from. The catalog is one of two ways to get a template, the other being to write it. This post does both.

1. Import PostgreSQL from the catalog

Open the project’s Catalog page and find PostgreSQL. Click import. The dialog asks for a name and a slug; keep the suggested postgres. Because you imported from the project’s own catalog page, the copy lands in the project, and a maintainer needs no global role to do it.

The project catalog page with the PostgreSQL entry and the import drawer open, scope fixed to the demo project

Import is a copy. The drawer says so, and the scope is the project because that is where you opened the catalog.

The entry you just copied is worth reading once, because it is the pattern your own templates will follow:

services:
  postgres:
    image: postgres:18-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
    volumes:
      - postgres-data:/var/lib/postgresql/18/docker
    healthcheck:
      test: [ "CMD-SHELL", "sh -c 'pg_isready -U $POSTGRES_USER -d $POSTGRES_DB'" ]
      start_period: 20s
      interval: 5s
      timeout: 5s
      retries: 12

Plain Compose. A pinned image tag, a named volume, and a healthcheck with a realistic start period. The healthcheck is not decoration: it is what lets the next step say “start the application after the database is ready” and mean it. The credentials are throwaway and stated in the open, which is right for an ephemeral environment and wrong for anything else.

2. Write a template for PetClinic

Now the other way to get a template. Go to the project’s Templates page and create one. Name it PetClinic, slug petclinic. The create form offers Start from skeletons for Spring Boot, Quarkus and Rails; PetClinic is Spring Boot, but the template is short enough to type, so paste this into the spec editor:

services:
  petclinic:
    image: springcommunity/spring-petclinic:3.5.6
    environment:
      SPRING_PROFILES_ACTIVE: postgres
      POSTGRES_URL: jdbc:postgresql://{{service.postgres.host}}:5432/{{service.postgres.env.POSTGRES_DB}}
      POSTGRES_USER: "{{service.postgres.env.POSTGRES_USER}}"
      POSTGRES_PASS: "{{service.postgres.env.POSTGRES_PASSWORD}}"
    labels:
      fadebox.ingress.port: "8080"

The Create Template drawer with the PetClinic spec in the editor, scoped to the demo project

The template editor with the spec above. The Start from menu offers Spring Boot, Quarkus and Rails skeletons for your own application.

Three things in it are Fadebox rather than Compose, and they are the whole lesson.

The image is pinned. 3.5.6, not latest. An untagged reference makes the daemon pull every tag, and a moving one makes two instances of the same environment run different code. The tag can be overridden per environment later, which is how dev and staging share this template and run different versions.

The database is wired by reference, not by literal. {{service.postgres.host}} resolves at deploy time to the in-instance hostname of whichever environment service is named postgres, and {{service.postgres.env.POSTGRES_DB}} to the effective value of that variable on it, overrides included. If someone later changes the database password on the environment, PetClinic follows. If the environment has no service called postgres, the deploy fails with a message that says so, instead of an application that starts and then cannot connect. The spec editor completes these references as you type, so you do not write them from memory.

One label asks for a URL. fadebox.ingress.port: "8080" says “this service speaks HTTP on 8080, give it a hostname”. There is no ports: entry and there should not be one: the ingress reaches the container over the Docker network, and a host-bound port is what a second instance would collide on.

One thing is missing, and deliberately. There is no healthcheck. The PetClinic image is built with Cloud Native Buildpacks and ships no shell, no curl and no wget, so there is nothing inside the container to run a probe with. That is common for modern JVM images and worth knowing before it bites you on your own. The consequence here is small: a service without a healthcheck counts as ready when it has started, and since nothing starts after PetClinic, nothing waits on it. For your own application, pick a run image with curl in it and probe the actuator; the Spring Boot skeleton in the create form has that healthcheck written out.

Save it. Fadebox parses the spec on the way in and reads it back: which service is the main one, which ports the ingress will publish, which references it carries. An unsupported Compose key is a hard error rather than a silent drop, because a silently ignored key is a deploy that quietly does the wrong thing.

3. Compose the environment

An environment composes templates into an application stack. It belongs to the project and it is a pure definition: creating one starts nothing.

Go to the project’s Environments page and create a new one. The page has three columns: the templates you can add on the left, the environment you are building in the middle, and what happens when you press the button on the right.

Name it Dev; the slug becomes dev. Click PostgreSQL and PetClinic in the left column. Each becomes an environment service in the middle, named after its template: postgres and petclinic. That name is what siblings resolve over the network and what prefixes the container name, and postgres is exactly the name the PetClinic template’s references expect. Had you named it db, the middle column would already be telling you that petclinic references a service not in this environment yet.

Set the waves: postgres in wave 0, petclinic in wave 1. Services in a wave start together, and a wave does not begin until the previous one is ready, where ready means healthy for services with a healthcheck. That number is how a database is guaranteed to accept connections before the application that needs it, with no wait-for-it script anywhere.

The right column decides what the button does. Definition only creates the environment and leaves instances for later. Choose Create & deploy instead, name the instance pr-42, confirm the runtime is local, and press it.

The New Environment page: templates on the left, postgres in wave 0 and petclinic in wave 1 in the middle, Create & deploy selected on the right with instance name pr-42

One page, three columns. The wave numbers next to each service are the whole start-order story.

4. Watch the instance come up

An instance is a running copy of an environment on a runtime. The deploy runs in the background; open the instance and the status panel fills in per container as the daemon reports it: pulled, created, started, healthy. Fadebox keeps no copy of that state. The daemon is the source of truth, which is why running instances survive a Fadebox restart.

postgres goes healthy first. Then wave 1 starts, and PetClinic takes a few seconds to create its schema and seed it. When the instance reaches RUNNING, the panel shows the URL:

http://demo-dev-pr-42-petclinic.localtest.me

The live status panel for instance pr-42: postgres running and healthy in wave 0, petclinic running in wave 1 with an Open link, and its startup log below

The status panel reads container state straight from the daemon. The Open link next to petclinic is the URL above.

Open it. Click Find owners and search with an empty name: ten owners, their pets, their visits, all read from the PostgreSQL that did not exist a minute ago.

Spring PetClinic’s Find Owners page listing owners, served from the instance URL

PetClinic answering on its instance hostname, data and all, routed by Traefik on the runtime host. Every container, network and volume this instance created carries its namespace, demo-dev-pr-42, in its name.

5. Deploy a second one

Back on the environment, create another instance, pr-43, and deploy it. Both are running on your machine now, on the same Docker daemon, each with its own PostgreSQL and its own data. Nothing is shared and nothing collides, because everything either instance created is prefixed with its own namespace. That is the ephemeral part: instances are copies of a definition, not branches of a shared stack.

The dev environment page with two instances, pr-42 and pr-43, both RUNNING on the local runtime

Two running copies of the same definition. Each has its own PostgreSQL, its own network and its own URL.

Add an owner in pr-42. It is not in pr-43. Stop pr-43, redeploy it, delete it. Since Fadebox 1.2 a redeploy restarts only the containers whose definition changed, so bumping PetClinic’s tag on the environment leaves its database running.

What you built, and what it is called

And one thing you did not build: nothing here is coupled to the catalog. Delete the bundled entry, register a different source, or take Fadebox offline; postgres in this project is a template you own and deploys as before.

Where to go next

Pin a version. The image tag lives on the environment service, not the template. Clone dev to dev-3-5-0, set PetClinic’s tag on the copy, and you have two version streams on one template. Or let the tag come from a file in git, {{git:app-repo:deploy/values.yaml#$.image.tag}}, so a release is a commit and never a write to Fadebox. The git value sources guide has it.

Put your own application in. Replace the image with yours, keep the references, add the healthcheck. The authoring guide has the supported Compose subset, the label directives and the init-container pattern for migrations.

Let a pipeline do step 3. Create an API key on the project and the CLI does what you just did by hand, on every pull request:

fadebox instance up pr-123 --wait

It finds or creates the instance, deploys it, waits for the containers to settle and prints the URL to paste into the merge request. The CLI guide covers login, contexts and JSON output.