Unisrv logoUnisrv.io
CLI Reference

unisrv.hcl

Reference for the declarative project file applied by unisrv up.

unisrv.hcl declares the desired state of one project: HTTP services (routing and domains), deployments (containers), and internal networks (private connectivity). It lives in the directory where you run unisrv up.

project = "myapp"

service "web" {
  deployment = "web"
}

deployment "web" {
  port = 8080
  container {
    image = "nginx:1.27"
  }
}

Top level

ElementFormRequired
projectproject = "name"yes
serviceservice "<name>" { ... }no, repeatable
deploymentdeployment "<name>" { ... }no, repeatable
networknetwork "<name>" { ... }no, repeatable

project must be a literal string — no interpolation. Block labels are unique per block type and must be DNS-safe: lowercase letters, digits and hyphens, no leading or trailing hyphen, 1–48 characters (^[a-z0-9]([a-z0-9-]*[a-z0-9])?$).

service

An HTTPS entrypoint that routes by path prefix to deployments or external URLs.

service "web" {
  hosts      = ["myapp.unisrv.dev", "www.example.com"]
  allow_http = false

  location "/api" {
    deployment = "api"
  }

  # Shorthand: catch-all "/" route, appended AFTER all explicit locations.
  deployment = "frontend"
}
AttributeTypeDefaultNotes
hostslist of strings[]Domains, claimed with unisrv host claim before unisrv up. Each binds to one service globally.
allow_httpboolfalseWhen false, plain HTTP redirects to HTTPS.
deploymentstringSugar for location "/" { ... }, appended after explicit locations so it never shadows them. Cannot be combined with an explicit location "/".
locationblock, repeatableRouting table, see below.

Host rules:

  • unisrv.dev domains must be a single alphanumeric label: myapp.unisrv.dev is valid, my-app.unisrv.dev and a.b.unisrv.dev are not.
  • External domains may contain hyphens freely, and need DNS pointed at the Unisrv edge.
  • Every service is always reachable at {service-name}-{environment-slug}.unisrv.dev without being declared, so hosts are only needed for a permanent address of your own.

location

location "/api" {
  deployment = "api"
}

location "/" {
  deployment   = "frontend"
  override_404 = "/index.html"   # SPA fallback
}

location "/legacy" {
  url = "https://old.example.com/app"
}

The label is a path prefix, matched in declaration order, first match wins. Exactly one target must be set:

AttributeMeaning
deploymentRoute to that deployment and bind it to this service. It must define port.
instance_groupEscape hatch: route to a raw instance group, for instances managed out-of-band.
urlReverse-proxy to an absolute http(s):// URL.

Optional override_404: a path (query allowed) on the same upstream to serve when it returns 404, typically "/index.html". Must be a path, not a full URL.

Path rules:

  • Must start with /; no ?, #, whitespace, or //.
  • No trailing / except the root "/". Prefixes match as raw strings, so /api already matches /api/users.
  • No duplicates, and a path that extends an earlier one is unreachable and rejected — order most specific first (/api/admin before /api).

deployment

N replica instances of one container image.

deployment "api" {
  port     = 8000
  replicas = 2
  network  = "internal"

  vcpus      = 1
  vcpu_ratio = 0.25
  memory     = "1GB"

  container {
    image = "registry.example.com/myapp/api:${var.image_tag}"
    args  = ["--workers", "4"]
    env = {
      DATABASE_URL = "postgres://app:${var.db_password}@postgres:5432/app"
      LOG_LEVEL    = "info"
    }
  }
}
AttributeTypeDefaultRangeNotes
portnumber1–65535The port the container listens on. Required if a location routes here; omit for workers.
replicasnumber10–100 keeps the deployment defined but runs nothing.
networkstringA network block to join. Omit if it needs no private connectivity.
vcpusnumber11–32vCPUs per instance.
vcpu_rationumber0.250.125, 0.25, 0.5, 1.0Guaranteed share of a physical core per vCPU. 1.0 is dedicated, lower is burstable.
memorynumber or string512 (MB)128MB–32GBBare number = MB. Strings take binary units, case-insensitive: "2GB", "2g". Fractions must land on a whole MB.
containerblockRequired. See below.

container

AttributeTypeRequiredNotes
imagestringyesFull OCI reference, already built and pushed — there is no build-from-source. Private registries need unisrv registry add. Prefer immutable tags or digests over latest.
argslist of stringsnoOverrides the image CMD.
envmap of stringsnoEnvironment variables. Use ${var.NAME} for secrets.

A deployment can be routed from at most one service (any number of its locations). Two services routing the same deployment is an error.

Storage is ephemeral. Scratch-only filesystems, no persistent volumes; anything written is lost when an instance is replaced by a redeploy, scale, or failure. Suitable for a dev or preview database, not durable data — use an external provider.

network

Private connectivity and name-based discovery between deployments.

network "internal" {
  iprange = "10.0.0.0/16"
}
AttributeTypeDefaultNotes
iprangestring"10.0.0.0/16"IPv4 CIDR, /16 or /24. Host bits must be zero (10.0.0.5/16 is rejected).

Service discovery

Within one network, a deployment's name resolves to the IPs of its running instances. No ports are involved: connect using the deployment name and whatever port the target listens on.

network "internal" {}

deployment "postgres" {
  network = "internal"
  container {
    image = "postgres:17"
    env   = { POSTGRES_PASSWORD = "${var.db_password}" }
  }
}

deployment "api" {
  port    = 8000
  network = "internal"        # must be the SAME network as postgres
  container {
    image = "myapp/api:${var.image_tag}"
    env   = { DATABASE_URL = "postgres://postgres:${var.db_password}@postgres:5432/postgres" }
  }
}

postgres needs no port and no service — those are for inbound HTTP only. A deployment without a network can neither resolve nor be resolved, and with replicas > 1 the name maps to all running instance IPs.

Variables

Any string except project may interpolate variables: "${var.NAME}" inside a template, or bare var.NAME as a whole value. All values are strings.

deployment "api" {
  container {
    image = "ghcr.io/myuser/api:${var.image_tag}"
    env = {
      API_KEY = var.api_key
    }
  }
}

Values are supplied at unisrv up time, never stored in the file:

  • --var NAME=value (repeatable)
  • --var-file FILE (repeatable; dotenv format: NAME=value lines, # comments)
  • An interactive prompt for any referenced-but-unset variable

Names are identifiers: a letter or _, then letters, digits, underscores. Every supplied variable must be referenced, and no name may be supplied twice — both are errors.

Use variables for all secrets and for image tags that change per release, and keep the var-file out of version control.

Before it applies

A valid file still fails at apply time if out-of-band setup is missing: every domain in hosts must be claimed with unisrv host claim, and private images need a credential from unisrv registry add. unisrv up shows a plan and asks before applying, including for destructive changes (renaming a service or network, changing an iprange, moving a deployment between services).

On this page