The provides field
Your app listens on a port. But the world outside your container doesn't know which one, what protocol it speaks, or whether it should be reachable from outside the deployment. The provides field makes all of that explicit.
Each entry in provides declares a single endpoint. The key properties are:
protocol— What your app's own listener speaks on this port:http,https,tcp,udp,grpc, orws. It is not the scheme a browser shows — that is$app.scheme, which the platform decides.port— The port number your app listens on inside the container.bind— Optional. The address to bind to (defaults to0.0.0.0).exposed— Whentrue, this port is meant to be reachable from outside the deployment — your app's public entry point. Whenfalse(the default), it's only reachable by other components in the same deployment.
Build it up
provides:
- protocol: http
port: 3000
exposed: truedata volume mounts at the given path and survives container restarts.provides:
- protocol: http
port: 3000
exposed: true
storage:
data:
path: /var/lib/app/data
persistent: trueprovides:
- protocol: http
port: 3000
exposed: true
storage:
data:
path: /var/lib/app/data
persistent: true
health: /healthprovides:
- protocol: http
port: 3000
exposed: true
storage:
data:
path: /var/lib/app/data
persistent: true
health:
path: /api/health
interval: 30s
timeout: 5s
retries: 3
start_period: 60sMore than one host name
Some apps serve different surfaces on different host names from one listener: a dashboard at dash., an API at api., or one name per tenant. The server reads the Host header and picks a surface. Launched on a single host, such an app starts, passes its health check, and cannot be used. at: tells the platform which names the listener answers at:
provides:
- name: https
protocol: https
port: 443
exposed: true
at: ["@", dash, api, "*", "*.*"]Every value is relative to the app host, the value of $app.host. You never write the domain itself — the platform decides that.
"@"— the app host itself.- A label, such as
dash— the namedash.<app host>. One lowercase DNS label, no dots. "*"— every name exactly one label below the app host. Use it when names are created while the app runs, such as one per tenant."*.*"— every name exactly two labels below the app host.
For a single name, at: dash is shorthand for at: [dash]. Quote "@", "*" and "*.*": YAML reads them as syntax otherwise.
An entry that declares at: answers at the listed names and no others. Leave out "@" and your app does not answer at the app host, so the platform can put something else there. A value may appear once in the whole app, so every name reaches exactly one listener: an exact label wins over a pattern. at: is valid only on an exposed: true entry that speaks http, https, ws or grpc.
launchfile up they print the names your app answers at and where requests arrive, for example dash.localhost on localhost:18080. Every request that reaches that port reaches your app with its Host header intact, so the only step left is making the name resolve: names under localhost often resolve to your machine already, and a line in your hosts file covers the rest. A platform that puts its own host-routing proxy in front of your app is different: it routes every name you list or refuses to launch, because no hosts file can fix a name its proxy drops. Your app builds its own addresses from $app.host, $app.scheme and $app.authority; at: adds no new expression.
Who fills the volume?
Most volumes start empty and the app fills them — a database directory, an upload folder, a cache. But some volumes hold content only you can supply: a music server's library, a photo manager's originals, the files a file manager browses. A provider can't tell the two apart from path and persistent alone, so it would happily create the library volume empty and start the app — a deploy that looks successful and serves nothing. Mark those volumes with content: operator.
storage:
music:
path: /music
persistent: truestorage:
music:
path: /music
content: operatorNot from the Launchfile — the host path (~/Music, /srv/books) changes per machine, so it never enters the file. You supply it at launch: launchfile up --storage music=~/Music (repeat the flag for multiple volumes). The provider binds your directory at the declared path, or refuses the component with an error naming the volume and the flag that would satisfy it. It never creates the volume empty. persistent doesn't apply to a marked volume — your directory outlives the deployment by construction.
Referencing the path
Declaring a volume gives it a path — but you rarely want to hardcode that path into your app's configuration. The volume may live at a different real path depending on where you deploy. Reference the path it was actually provisioned at with the $storage.<name>.path expression instead of repeating the literal path.
storage:
data:
path: /var/lib/app/data
env:
DATA_DIR: /var/lib/app/datastorage:
data:
path: /var/lib/app/data
env:
DATA_DIR: $storage.data.pathThe two copies can drift. Change storage.data.path but forget the duplicate in env, and your app reads from the wrong directory — silently. $storage.<name>.path always resolves to wherever your volume was actually provisioned, so the same Launchfile runs correctly anywhere with no edits. Under the hood: where the volume actually lives differs by deployment — a container provider bind-mounts it at the declared path, while a native dev provider provisions a separate host directory — and the expression hides that difference. Catalog apps like Mailpit and AnythingLLM use it to wire their data paths into the environment.
Health check deep dive
The shorthand form covers most cases: give it a path and the rest is handled for you. When you need more control, expand it into an object.
health: /healthhealth:
path: /api/health
interval: 30s
timeout: 5s
retries: 3
start_period: 60sThe start_period field gives your app time to boot before failed health checks start counting against it. A Java app loading Spring context or a Rails app compiling assets might need 60–120 seconds. During the start period, failed health checks don't count toward the retry limit.
Not every app speaks HTTP. A background worker or a TCP-only service has nothing for a path check to hit — but it can still tell you it's alive. Use command instead of path: the provider runs the shell command, and a zero exit code means healthy.
health:
path: /api/health
retries: 3health:
command: "pg_isready -q"
retries: 3path takes precedence over command — declare one or the other. And if your app has no meaningful health signal at all, you can omit health entirely: liveness falls back to a simpler signal, like whether the process is still running.
In the wild
Ghost is a professional publishing platform. Its Launchfile declares an HTTP endpoint, persistent storage for uploaded content, and a health check hitting the admin API.
version: launch/v1
name: ghost
description: "Professional publishing platform"
repository: https://github.com/TryGhost/Ghost
logo: https://ghost.org/images/logos/ghost-logo-orb.png
image: ghost:5-alpine
provides:
- protocol: http
port: 2368
exposed: true
requires:
- type: mysql
set_env:
database__connection__host: $host
database__connection__user: $user
database__connection__password: $password
database__connection__database: $name
database__connection__port: $port
env:
database__client:
default: "mysql"
url:
default: $app.url
required: true
description: "Public URL of the Ghost instance"
storage:
content:
path: /var/lib/ghost/content
persistent: true
health: /ghost/api/v4/admin/site/
restart: alwaysprovides:- Declares an HTTP endpoint on port 2368, publicly exposed so readers can reach the blog.
storage:- The content volume at /var/lib/ghost/content holds themes, images, and uploads. Persistent means it survives redeploys.
health:- Hits the Ghost admin site endpoint. A 2xx means the app is ready to serve traffic.
Try it: npx launchfile up ghost to launch this app locally.View in catalog
Check your understanding
providesdeclares every network endpoint your app exposes, with protocol, port, and visibility.storagedeclares named volumes. Setpersistent: truefor data that must survive restarts.content: operatormarks a volume you fill yourself — the provider binds your directory (--storage <volume>=<path>) or refuses the component, and never creates the volume empty.$storage.<name>.pathresolves to the real path the volume was actually provisioned at — reference it inenvinstead of hardcoding the path in two places.healthdescribes how your app's readiness is checked. Use the string shorthand for simple cases, the object form for tuning — withpathfor HTTP checks orcommandfor everything else.exposed: truemeans "something outside the deployment should reach this." Where that reaches, and whether it is encrypted, are the platform's call. Default isfalse(internal only).