Environment Variables

Every service carries an env map. Variables are applied at build time and at runtime, so a variable set here is visible to both the build and the container.

services:
  - name: api
    source:
      type: image
      image: nginx:latest
    env:
      NODE_ENV: production
      DATABASE_URL: ${{ db.DATABASE_URL }}

Plain values

A value with no ${{ }} token is passed through as written. Secrets in env are never written to logs or error messages.

Host environment expansion

A bare $VAR or ${VAR} is expanded from the shell environment where you run up apply. Use it for values you do not want to commit:

export GHCR_TOKEN=ghp_...
registries:
  - name: ghcr
    host: ghcr.io
    password: $GHCR_TOKEN

An unresolvable host variable is an error, so a typo fails loudly at apply time instead of shipping an empty secret.

References between services

Use a ${{ }} template to read a variable from another service. This is the Railway-style form and the only supported reference syntax:

services:
  - name: db
    source:
      type: image
      image: postgres:16
    env:
      POSTGRES_DB: app

  - name: api
    source:
      type: image
      image: myapp:1.2.3
    env:
      DATABASE_URL: postgres://postgres@${{ db.HOST }}/app

Reference yourself with ${{ self.NAME }} or a bare ${{ NAME }}. Tokens may be embedded in a larger value, as in the DATABASE_URL example above.

self always means the service that declares the value, not the service that references it. A ${{ other.VAR }} value may itself contain self. tokens, and those resolve against other. This lets a service own a fully-formed connection string built from its own variables and then share it with a consumer in one reference:

services:
  - name: postgres
    source:
      type: image
      image: postgres:16
    env:
      POSTGRES_USER: karabo
      POSTGRES_PASSWORD: $POSTGRES_PASSWORD
      POSTGRES_URL: postgres://${{ self.POSTGRES_USER }}:${{ self.POSTGRES_PASSWORD }}@${{ self.UP_PRIVATE_DOMAIN }}:5432/karabo

  - name: api
    source:
      type: image
      image: myapp:1.2.3
    env:
      DATABASE_URL: ${{ postgres.POSTGRES_URL }}

The legacy $db.HOST / ${db.HOST} form is rejected with a hint to use ${{ }}, and a ${{ }} that names an unknown service or variable is an error.

Injected system variables

up injects a set of reserved UP_* variables into every service. You can read them in config, but a user key of the same name never overrides them.

UP_SERVICE_NAME

The name of the service.

api

UP_PROJECT_NAME

The name of the project.

myapp

UP_ENVIRONMENT_NAME

The name of the environment.

production

UP_PRIVATE_DOMAIN

The private DNS name of the service on the environment network.

api.up.internal

UP_PUBLIC_DOMAIN

The first configured domain, if any. Empty when the service has no domains.

api.myapp.com

UP_PRIVATE_URL

The private URL, including the effective container port.

http://api.up.internal:8080

UP_PUBLIC_URL

The public URL. The port is omitted because the proxy serves 80/443.

https://api.myapp.com

Injected variables are excluded from diffing, so they never cause a perpetual redeploy.

Dependencies and ordering

A cross-service reference creates a dependency edge. up deploys dependencies first (topological order), so the database is running and healthy before the API that references it is deployed.

Circular references are rejected with the cycle path. References to a job service are also rejected: only long-running services may be linked, and a job may reference a service but nothing may reference a job.

Build-time vs runtime

The same env map feeds the build and the container, so a variable set here is available to both.

For a railpack build, the variables enter the Railpack build environment, where RAILPACK_* variables configure the build. See the Railpack documentation for the full list.

References are resolved once at apply time and again at deploy time, so the container receives resolved values even for services created imperatively with up service create.