Skip to main content
Your spec references values with ${...} 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
  • command and args
The exception is the secrets slipway creates for a data service: a database’s password, its additional login roles, and a bucket’s access keys. They are never injected into other services, so creating a database does not hand its password to every app or mark them as needing a redeploy. A service that needs one asks for it, with ${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 as NODE_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
The console lists every hardcoded or computed key read-only under Defined in spec, so you can see what the spec already handles.

How values reach your container

Resolved env 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
Every value is also available as a BuildKit secret, for a credential the image’s history must never record:
Dockerfile
A build argument your Dockerfile writes to disk, such as RUN echo $TOKEN > /etc/secret, is baked into the image layer, and every ARG value is recorded in the image’s history. slipway cannot prevent either. Declare ARG for values that are safe inside the image, such as NEXT_PUBLIC_*, and read credentials through a secret mount instead.

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.
Leaving the page with unsaved rows prompts before the draft is lost. A secret’s value is masked and cannot be read back; to change one, type a new value and save. A value can span several lines. Paste a PEM key or a JSON credential and the field grows to show it, line breaks intact, and the container receives it exactly as pasted. Anything your app would rather read from disk, such as a certificate or a service-account file, can also be uploaded under Secret files on the same tab. Removing a row is staged too, and removing a stored secret asks once more when you save, because the value cannot be recovered.

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.