Unisrv logoUnisrv.io
CLI Reference

unisrv.hcl

Reference for the declarative project file applied by unisrv up.

unisrv.hcl describes one project. unisrv up reads it from the current directory, shows a plan, and makes the environment match.

BlockDeclaresPrimitive
serviceAn HTTPS entrypoint and its routing tableServices
deploymentReplicas of one container image, optionally with a volumeDeployments
networkA private network deployments join to reach each otherInternal networks

Example

A frontend and an API behind one domain, and Postgres on a private network with a persistent disk.

project = "webshop"

service "web" {
  hosts = ["webshop.example.com"]

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

  deployment = "frontend" # everything else
}

deployment "frontend" {
  port = 3000
  container {
    image = "ghcr.io/acme/webshop-frontend:${var.tag}"
  }
}

deployment "api" {
  port     = 8000
  replicas = 2
  network  = "internal"
  memory   = "1GB"
  container {
    image = "ghcr.io/acme/webshop-api:${var.tag}"
    env = {
      DATABASE_URL = "postgres://postgres:${var.db_password}@postgres:5432/postgres"
    }
  }
}

deployment "postgres" {
  network = "internal"
  container {
    image = "postgres:17"
    env   = { POSTGRES_PASSWORD = var.db_password }
  }
  volume {
    mount_path = "/var/lib/postgresql/data"
    size       = "10GB"
  }
}

network "internal" {}
unisrv up --var tag=v1.4.0 --var-file secrets.env

File rules

  • project is required and must be a plain string. No variables.
  • Block labels are lowercase letters, digits and hyphens, 1 to 48 characters, with no hyphen at either end. Each label is unique per block type.
  • Unknown attributes are errors. replica = 2 fails instead of being ignored.

service

service "web" {
  hosts      = ["webshop.example.com"]
  allow_http = false

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

  deployment = "frontend"
}
AttributeTypeDefaultNotes
hostslist of strings[]Your own domains. Each must be claimed first and belongs to one service.
allow_httpboolfalseWhen false, plain HTTP redirects to HTTPS.
deploymentstringShorthand for a catch-all location "/". Can't be combined with an explicit "/".
locationblockOne per path prefix. See below.

Every service is also reachable at {service}-{slug}.unisrv.dev, using its environment's slug.

Hosts. Matched case-insensitively, with any trailing dot dropped. Under unisrv.dev a host must be a single label of lowercase letters and digits: webshop.unisrv.dev works, my-webshop.unisrv.dev and a.b.unisrv.dev don't. Other domains need DNS pointed at the Unisrv edge before they're claimed.

location

The label is a path prefix. The longest prefix that matches wins, so the order you write locations in doesn't matter.

Each location sets exactly one target:

TargetRoutes toRequest path
deploymentA deployment in this file. It must set port.Forwarded unchanged
urlAn absolute http:// or https:// URLPrefix stripped, the rest appended to the URL's path
instance_groupInstances you start through the API with that group name, not managed by this fileForwarded unchanged

Optionally, override_404 serves another path from the same target when it returns 404. Use "/index.html" for single-page apps. It must be a path, not a URL.

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

# /legacy/users?page=2 → https://old.example.com/users?page=2
location "/legacy" {
  url = "https://old.example.com"
}

Path rules:

  • Starts with /. No ?, #, whitespace or //.
  • Prefixes match as raw strings: /api matches /api, /api/users and /apiv2. Use /api/ to match only the subtree.

deployment

deployment "api" {
  port       = 8000
  replicas   = 2
  network    = "internal"
  vcpus      = 1
  vcpu_ratio = 0.25
  memory     = "1GB"

  container {
    image = "ghcr.io/acme/webshop-api:${var.tag}"
    args  = ["--workers", "4"]
    env   = { LOG_LEVEL = "info" }
  }
}
AttributeTypeDefaultRangeNotes
containerblockRequired. See below.
portnumber1 to 65535Port the container listens on. Required if a location routes here.
replicasnumber10 to 100 keeps the deployment but runs nothing.
networkstringA network block in this file.
vcpusnumber11 to 32Per instance.
vcpu_rationumber0.250.125, 0.25, 0.5, 1.0Guaranteed share of a physical core per vCPU. 1.0 is dedicated.
memorynumber or string512128MB to 32GBPer instance. See sizes.
volumeblockA persistent disk per replica. See below.

A deployment can be routed by only one service, from any number of its locations.

container

AttributeTypeRequiredNotes
imagestringyesA pushed OCI image. There is no build step. Private registries need unisrv registry add.
argslist of stringsnoReplaces the image's CMD.
envmap of stringsnoEnvironment variables. Pass secrets through variables.

A moving tag like :latest is only pulled when instances are replaced. After re-pushing one, run unisrv up --invalidate. Immutable tags avoid the problem.

The container's own filesystem is thrown away whenever an instance is replaced. Only a volume survives.

volume

volume {
  mount_path = "/var/lib/postgresql/data"
  size       = "10GB"
}
AttributeTypeRequiredRangeNotes
mount_pathstringyesAbsolute path in the container. Not /, no ...
sizenumber or stringyes32MB to 256GBPer replica. See sizes.

The block is a template. Each replica gets its own volume, numbered from 0, and gets the same one back after restarts, rollouts and scaling. replicas = 3 with size = "10GB" means three 10GB disks.

  • Rollouts stop the old instance before starting the new one, since a volume has one writer. Expect a short gap.
  • A volume stays on the node it was first written to, and so does its replica.
  • Scaling down keeps the unused volumes and gives them back when you scale up.

The volume is fixed at creation. Adding, removing or changing the block replaces the deployment, and the new one starts with empty volumes. The old ones are kept in the environment with their data but never reattached. See what unisrv up replaces.

Sizes

memory and size take a bare number of megabytes, or a string with M, MB, G or GB (any case). Units are binary: "1GB" is 1024MB. Fractions are fine if they land on a whole MB: "1.5GB" works.

network

network "internal" {
  iprange = "10.0.0.0/16"
}
AttributeTypeDefaultNotes
iprangestring"10.0.0.0/16"IPv4 CIDR. Host bits must be zero: 10.0.0.5/16 is rejected.

Deployments on the same network reach each other by name. postgres resolves to the IPs of the postgres deployment's running instances, one per replica. There are no ports to declare: connect on whatever port the target listens on.

A deployment without a network can't resolve other deployments or be resolved.

Variables

Any string except project can use variables: "${var.name}" inside a string, or var.name as the whole value. Values are always strings.

image = "ghcr.io/acme/api:${var.tag}"
env   = { API_KEY = var.api_key }

Supply them when you run unisrv up:

SourceFormat
--var name=valueRepeatable.
--var-file FILERepeatable. name=value lines, # comments.
PromptAsked for any unset variable. Without a terminal, an unset variable is an error.
  • There is no override order. Setting a name twice, in any source, is an error.
  • Unused variables are ignored.

Keep secrets in a var file outside version control.

What unisrv up changes

Most edits update resources in place. A few can only be applied by deleting and recreating.

ChangeResult
Image, env, args, resources, replicas, portRolling update
A deployment's networkRolling update onto the new network
Hosts, locations, allow_httpUpdated in place
Moving a deployment to another serviceDeployment replaced¹
Adding, removing or changing a volumeDeployment replaced¹
A network's iprangeNetwork replaced, and every deployment on it¹
Renaming any blockOld one deleted, new one created

¹ A replaced deployment starts on empty volumes. The old ones stay in the environment with their data.

A private image without a registry credential passes the plan but fails when its instances start.

Confirmations:

  • Every plan asks before applying. -y answers yes. It's required when there's no terminal, as in CI.
  • A plan that leaves volumes behind lists them and asks you to type destroy. -y doesn't answer this. --allow-data-loss does, so an unattended run needs both.
  • Removing a deployment block doesn't ask. Its volumes stay in the environment with their data, and their cost, until unisrv destroy.

On this page