${...} and slipway resolves them at deploy time. There
are two stores.
A reference nothing satisfies does not fail the deploy: the deploy log records a warning
and the referencing key is left unset (an env value that mixes a missing reference into a
longer string keeps the string with the reference removed). Two exceptions still fail
fast: a managed database’s password env — an engine must never come up with a blank
credential — and, in specs converted from a committed docker-compose file, the required
form
${NAME:?message}, which is an explicit “do not deploy without this” marker.
Scopes
Every value is set on a service’s Environment tab, and applies either to that one service or to every service in the project.
A service value wins over a project value of the same name, and the bare form
${secret.NAME} resolves in that order. There is no workspace-wide tier: a spec using
${secret.org.NAME} or ${var.org.NAME} is rejected at validation, naming the project
scope as its replacement.
You do not need a reference for the common case. Every service and project value is
injected into the container as an environment variable on the next deploy, so setting
DATABASE_HOST on the Environment tab is all it takes. Reach for ${...} only where a
plain environment variable cannot go:
buildArgs, which run at build time and are deliberately not injected (feeding values into a build moves image content and invalidates the layer cache)- a value composed into a longer string, like
DATABASE_URL: postgres://app:${secret.PG_PASSWORD}@db:5432/app commandandargs
${service.<db>.password} (or
.url) or an explicit ${secret.NAME}.
A key declared in the spec’s env: block wins over a stored value of the same name. The
spec is an explicit statement about that service, so a dashboard row cannot quietly
override it. A value the spec shadows this way is never pending: the Stale badge names
only values a deploy would apply.
The project scope keeps its historical env spelling in the substitution syntax.
Each deploy resolves the current values, so a service picks up an edit on its next
redeploy — which is what the Stale badge on that service is telling you. Restart does
not apply it: a restart replaces the pods with the config they deployed with. Redeploy
is what applies saved changes.
The badge is per service, because a deploy is per service: edit a variable both api and
web read, redeploy api, and web stays stale until you deploy it too. Click the badge
to see what would change. Variables show their old and new values; secrets and secret
files are named but never shown.
Syntax
slipway.yaml
Names match
^[A-Z][A-Z0-9_]*$.
Substitution happens in env and buildArgs only. Elsewhere in the spec a ${...}
expression is left as literal text.
A literal in the spec always wins
A key with a plain value in the spec, such asNODE_ENV: production, is not a reference. A
variable or secret of the same name never overrides it, so the running configuration cannot
drift from what the repository says.
To let a value vary per project, make it a reference:
slipway.yaml
How values reach your container
Resolvedenv values are injected into the container’s environment at startup. They do not
appear in the pod definition, the deployment row, or anywhere a viewer would see them.
Resolved buildArgs are passed to the build as build arguments, never on the build command
line.
Resolved env values reach the build too. A language runtime exposes every one of them to
its dependency install and its build command, which is how a framework that inlines values
at build time, such as Next.js with NEXT_PUBLIC_*, gets them. Changing a value rebuilds
those steps rather than reusing their cached layers. Nothing is written into the image: the
values travel as BuildKit secrets, mounted only for the duration of each step.
A committed Dockerfile receives the values it asks for. Declare a name with ARG and the
service’s value arrives as that build argument, the same convention Render and Railway use:
Dockerfile
Dockerfile
Setting values
Both stores live on a service’s Environment tab. Each row says whether it applies to that one service or to every service in the project, and the shared ones are also editable on the project’s Settings → Variables & secrets. Each list ends in a New variable or New secret row. Select it, give the row an uppercase name and a value, and a bar appears at the bottom of the page counting what is unsaved. Nothing is written until you choose one of its two actions:- Save stores the values. Running services keep what they deployed with and show the Stale badge until you deploy them.
- Save and deploy stores the values and redeploys, at the commit already running. On a service’s tab that is the one service; on the project’s tab it is every service the save left stale, and nothing else.
Importing a .env file
Every place you manage values has an Import .env button. Upload or paste a file and slipway parses it in your browser: nothing is sent anywhere until you confirm. You then review each entry, untick what you do not want, rename or edit values, and choose per entry whether it becomes a secret or a variable. Names that look sensitive default to secret. Entries that would overwrite an existing value are flagged, and overwriting a stored secret is opt-in. Imported entries land as unsaved rows in the same lists, counted by the same bar. Save stores them, and Save and deploy stores them and applies them in one step.Finding what a spec needs
A service’s Spec view has a secrets manager below the rendered spec. It reads the effective spec the way a deploy does, lists every${secret.*} and ${var.*} it
references, and shows each as set, with the scope it resolves from, or missing. Set any
missing value inline, which is the quickest way to get a new repo configured before its
first deploy.
Per-service provenance
Expand a service’s env on the project page to see every variable it receives, tagged with where it resolves from:spec for a literal, env for a project value, org for an
organization value, and unset for a reference nothing satisfies yet.
Secret values stay masked. Variable and literal values are shown inline.
What is logged
Every deploy records what it resolved.
To keep a value out of the event log, make it a secret.
Preview values
Both stores can carry a preview-only value, resolved instead of the production value inside a preview. Production is untouched.Renaming
Edit a saved variable’s or secret’s name in place on the service’s Environment tab and save. The value moves with it (including a preview-only value), and a secret is renamed without you re-entering it. A rename keeps the row in the same scope; to move a value between this service and the whole project, delete it and add it again. A rename does not rewrite references. Anything that reads${var.OLD} or ${secret.OLD} —
in slipway.yaml or in another variable’s value — no longer resolves after the rename, and the
next deploy warns about it. Running services keep the old name until their next deploy.
Permissions
Secrets are write-only in the ordinary UI: names are listed, values are not. The one read
path is the admin-gated reveal on a project secret — what the database connection card
uses to show a provisioned password — and it resolves reference-shaped values
(
${service.db.password}, ${secret.NAME}) down to the concrete credential.