---
title: "Smolfile"
description: "Declarative local machine configuration."
canonical: "https://smolmachines.com/docs/introduction/concepts/smolfile"
source: "https://github.com/smol-machines/docs/blob/main/introduction/concepts/smolfile.md"
---

# Smolfile

A `Smolfile` is a TOML file that declaratively describes a machine workload. It keeps the image, command, resources, networking, development settings, health checks, and selected capabilities in one reviewable file.

The file is conventionally named `Smolfile` (without an extension) and lives in the project root. Pass another path with `-s` or `--smolfile`:

```bash
smolvm machine run -s Smolfile
smolvm machine create --name my-machine --smolfile ./deploy/Smolfile
```

Smolfile rejects unknown fields. A misspelled or unsupported key is a parse error, rather than an ignored setting.

## Minimal example

```toml
image = "python:3.12-alpine"
cmd = ["python", "-m", "http.server", "8080"]
cpus = 2
memory = 2048
net = true

[dev]
volumes = ["./src:/app"]
ports = ["8080:8080"]
workdir = "/app"
```

## Workload fields

These fields describe the machine and its workload. All are optional.

| Field | Type | Description |
| --- | --- | --- |
| `image` | string | OCI image reference, or a path to a local `docker save` archive or unpacked rootfs. Omit it for a bare Alpine VM. |
| `entrypoint` | string array | Executable and fixed arguments. Overrides the image's `ENTRYPOINT`. |
| `cmd` | string array | Default arguments. Overrides the image's `CMD`. |
| `env` | string array | Environment variables written as `KEY=VALUE`. |
| `workdir` | string | Working directory inside the VM. |
| `user` | string | User the workload runs as: a name from the image's passwd database, or a numeric `uid[:gid]`. Overrides the image's `USER`. |
| `cpus` | integer | Number of vCPUs. Default: `4`. |
| `memory` | integer | Memory in MiB. Default: `8192`. |
| `storage` | integer | Storage disk size in GiB. |
| `overlay` | integer | Overlay disk size in GiB. |
| `block_io` | string | Host block I/O engine, `"sync"` or `"async"`. Default: `"sync"`. |
| `disk_durability` | string | `"full"` makes a guest `fsync` wait for the host disk; `"deferred"` lets writes reach it on the host's schedule, and a host crash can lose recent writes. Default: `"full"`. |
| `stop_signal` | string | Signal the workload gets when the machine stops, as a name such as `"SIGINT"` or a number. Default: `"SIGTERM"`. |
| `stop_timeout` | integer | Seconds the workload gets to exit after its stop signal before it is killed. Default: `10`. |
| `net` | boolean | Enable outbound networking. Networking is off by default. |
| `gpu` | boolean | Enable Vulkan GPU acceleration through virtio-gpu. |
| `gpu_vram` | integer | GPU shared-memory size in MiB. Ignored unless `gpu = true`. |
| `cuda` | boolean | Enable CUDA-over-vsock on supported Linux hosts with an NVIDIA driver. |
| `auto_graph` | boolean | Ask compatible CUDA frameworks to capture safe graph regions. Implies `cuda`. |
| `rosetta` | boolean | Enable Rosetta 2 translation for x86_64 binaries on Apple Silicon macOS. |
| `docker_socket` | boolean | Expose the guest Docker socket to the host as a Unix socket. |
| `net_backend` | string | Networking backend: `"tsi"` or `"virtio-net"`. |

`image` resolves the same way as the `--image` flag, so it takes a locally built
archive as well as a registry reference:

```toml
image = "./myapp.tar"
```

A bare name is always a registry reference. A path, or a name ending in an
archive suffix, is read as a `docker save` archive, and an existing directory is
read as an unpacked rootfs. `file://` is not a supported prefix and is rejected
with a message telling you to give the path directly.

At import the archive's architecture is checked against the guest: an archive
built for another CPU is refused with a message naming both, and on Apple
Silicon an `amd64` archive is accepted when Rosetta is enabled.

A local archive needs no networking to start, which is the offline path for a
machine built elsewhere.

`entrypoint` and `cmd` follow Docker/OCI semantics. If they are omitted, the image's built-in values are used. A command supplied after `--` replaces both Smolfile fields.

### Choosing the workload user

`user` sets the account the workload runs as, in the same form `docker run --user`
takes. The usual reason to set it is a mounted host directory: an image built for
`root` writes files the host user then cannot edit, and naming the mount's owner
here avoids that.

```toml
image = "python:3.12-alpine"
user = "1000:1000"
```

It overrides the image's own `USER`. The CLI equivalent is `--user` on
`machine run` and `machine create`, and a flag beats the Smolfile when both are
given.

::: warning Init commands always run as root
`init` provisions the machine, so it runs as root whatever `user` says and
whatever the image's `USER` is. That is deliberate: package installs and mounts
need the privilege. Only the workload runs as `user`.
:::

## Development profile

`[dev]` contains local-development settings. It is used by machine run/create workflows and is not included in packed artifacts.

| Field | Type | Description |
| --- | --- | --- |
| `volumes` | string array | Host bind mounts, such as `"./src:/app"`, or a guest path alone, such as `"/var/lib/docker"`, for a directory on the machine's own disk. |
| `ports` | string array | Port mappings, such as `"8080:8080"`, or `"27015:27015/udp"` for UDP. |
| `env` | string array | Development-only `KEY=VALUE` variables. |
| `init` | string array | Commands run once, on first start. |
| `workdir` | string | Development-only working directory. |
| `user` | string | Development-only user override, in the same form as the top-level `user`. |

```toml
[dev]
volumes = ["./src:/app"]
env = ["APP_MODE=development"]
init = ["pip install -r requirements.txt"]
ports = ["8080:8080"]
workdir = "/app"
```

The legacy top-level `volumes`, `ports`, and `init` fields are also accepted. Prefer the `[dev]` fields for new Smolfiles.

### When init runs

`init` runs once, on the machine's first start, not on every start. Later starts
skip it and say so:

```text
Init already completed, skipping 3 command(s)
```

That is why init is the place for provisioning that should happen once, such as
installing packages, and not for anything a restart needs to redo. A machine
restored from a checkpoint counts as already initialized, because the restored
memory already contains the provisioned guest.

For an ephemeral run, `image` plus `init` is baked once into a cached artifact
and later runs of the same pair start from it. Files init wrote under the
working directory are captured with it, so a cached run does not start with them
missing. Pass `--no-init-cache` when init depends on live volume contents and
cannot safely be reused, or `--rebuild-init-cache` to rebuild it once. When an init command fails, the run prints that
command's output and its exit status.

The same commands can be given on the command line with `--init`, which is
accepted on `machine run` as well as `machine create`. When a Smolfile also sets
`init`, both run: the Smolfile's commands first, then the flag's.

### Secrets and init do not mix on the cached path

An ephemeral run that has `init` steps is cached by default. smolvm bakes `image` plus `init` once
into a reusable artifact and later runs of the same pair rehydrate from it, printing
`Using cached init layer <id>`. Nothing has to be passed to opt in, so a Smolfile with `[init]`
takes that path on its second run whether or not you know the cache exists.

**A secret that `init` needs does not survive that path.** The bake runs without the secret refs,
and what happens next depends on how the secret was given:

- A Smolfile `[secrets]` entry with `[init]` **fails the run**, with an error about packs that
  never mentions the cache: `secret 'MYSEC': source kind 'env' is not allowed in scope Untrusted;
  secret refs may only be resolved on the trusted local host (packs may not carry secret refs)`.
- `--secret-env` or `--secret-file` on the command line is worse, because it does not fail. The
  init step runs with the variable **empty**, and the value is baked in that way, so every later
  run rehydrates the same empty result.

Pass `--no-init-cache` when `init` needs a secret. It re-runs `init` on every ephemeral run
instead of baking it, and the secret is present:

```bash
smolvm machine run -s app.smolfile --no-init-cache -- ./start.sh
```

This applies to ephemeral runs. A persistent machine runs `init` on its first start, outside the
init-layer cache, and is unaffected.

## Network policy

`[network]` narrows outbound access. Each field implies networking when it is set.

| Field | Type | Description |
| --- | --- | --- |
| `allow_hosts` | string array | Hostnames, with their subdomains, whose resolved IP addresses may be reached. Setting it selects the `virtio-net` backend, because the allow list is enforced by the host-side gateway. |
| `allow_cidrs` | string array | Allowed IP addresses or CIDR ranges, such as `"10.0.0.0/8"`. Setting it also selects the `virtio-net` backend, for the same reason. |
| `block_private` | boolean | Block private, internal and link-local destinations while the public internet stays open. Cloud metadata is blocked either way. |
| `allow_private` | string array | Private ranges to reopen under `block_private`, which it implies. |
| `allow_host_patterns` | string array | Exact hostnames, or `*.domain` for subdomains only. |
| `allow_udp` | string array | UDP destinations as `CIDR[:PORT[-END]]`. |
| `deny_cidrs` | string array | CIDR ranges refused before any allow is checked, so a deny wins over every allow list. |

```toml
net = true

[network]
allow_hosts = ["pypi.org", "files.pythonhosted.org"]
allow_cidrs = ["10.0.0.0/8"]
```

Hostnames are resolved when the VM starts. Use an allowlist instead of unrestricted `net = true` when the workload only needs a few destinations. To change the list of an existing machine, run `smolvm machine update` with `--allow-host` and `--remove-allow-host`, or `--allow-cidr` and `--remove-allow-cidr`; see [Update a machine](/docs/local/machine-lifecycle-cli-reference#update-a-machine).

### Choosing a networking backend

`net_backend` picks how the guest reaches the network. It takes the same two
values as the `--net-backend` flag, and the flag and the field are parsed by the
same code, so the spellings cannot drift apart.

| Value | What it is |
| --- | --- |
| `tsi` | libkrun's transparent socket layer. Outbound connections only. |
| `virtio-net` | A virtual interface served by the host-side network stack. |

`tsi` is the default and is enough for a workload that only makes outbound
connections. Choose `virtio-net` when the guest needs a real network interface
and a default route of its own, which is what anything doing its own routing
requires: a kernel-mode VPN client such as Tailscale is the usual case.

```toml
image = "alpine"
net = true
net_backend = "virtio-net"
```

Publishing a port needs `virtio-net` too, and you do not have to ask for it: with no
`net_backend` set the engine selects `virtio-net` itself. It refuses only when `tsi` was named
explicitly, because TSI is outbound only. So set the field when the guest needs the interface
without publishing anything, and leave it alone when a published port or an allow list already
implies it.

## Artifact profile

`[artifact]` overrides values when `smolvm pack create` builds a `.smolmachine` artifact. `[pack]` is an alias.

| Field | Type | Description |
| --- | --- | --- |
| `cpus` | integer | vCPU count for the artifact. |
| `memory` | integer | Memory in MiB for the artifact. |
| `entrypoint` | string array | Artifact entrypoint override. |
| `cmd` | string array | Artifact command override. |
| `oci_platform` | string | Target OCI platform, such as `"linux/amd64"`. |

```toml
[artifact]
cpus = 4
memory = 4096
oci_platform = "linux/amd64"
```

Development mounts and development initialization belong in `[dev]`; they are not part of the packed artifact.

## Health checks

`[health]` configures checks for `machine monitor`. `exec` is invoked directly as a command and argument array. To use shell syntax, set it to `['sh', '-c', '...']`.

| Field | Type | Description |
| --- | --- | --- |
| `exec` | string array | Command to execute. |
| `interval` | string | Time between checks, such as `"10s"`. |
| `timeout` | string | Maximum duration of one check. |
| `retries` | integer | Consecutive failures before the machine is unhealthy. |
| `startup_grace` | string | Delay before the first check. |

```toml
[health]
exec = ["wget", "-q", "-O", "/dev/null", "http://127.0.0.1:8080/health"]  # runs in the agent, which has no curl
interval = "10s"
timeout = "2s"
retries = 3
startup_grace = "20s"
```

## Restart policy

`[restart]` controls restart behavior for managed workloads.

| Field | Type | Description |
| --- | --- | --- |
| `policy` | string | `"never"`, `"always"`, `"on-failure"`, or `"unless-stopped"`. |
| `max_retries` | integer | Maximum restart attempts. |
| `max_backoff` | string | Maximum delay between restarts, such as `"60s"`. |

## Branching

`[branch]` configures a copy-on-write branch source and CUDA clone capacity. The older `[fork]` section name is still accepted.

| Field | Type | Description |
| --- | --- | --- |
| `enabled` | boolean | Start the machine as a branch source. |
| `pool_size` | integer | Planned number of runnable CUDA clones. Implies `enabled` and requires `cuda = true` or `auto_graph = true`. |
| `cuda_vram_limit_mib` | integer | Logical VRAM limit per source machine or clone. Requires `pool_size`. |

## Authentication and secrets

`[auth]` can forward the host SSH agent without copying private keys into the VM:

```toml
[auth]
ssh_agent = true
```

`[secrets]` maps guest environment variable names to host-side references. Never put plaintext secret values in a Smolfile.

```toml
[secrets]
DATABASE_URL = { from_env = "PROD_DATABASE_URL" }
TLS_KEY = { from_file = "/absolute/path/to/tls.key" }
NPM_TOKEN = { from_keychain = "npm/ci" }
```

`from_env` reads a host environment variable, `from_file` reads an absolute host file path, and `from_keychain` reads a macOS Keychain or Linux Secret Service item, `SERVICE[/ACCOUNT]`, at workload launch. The resolved plaintext is injected into the guest, while the reference is stored in the machine record and packed artifact so it can be resolved again on a trusted local host. See [Isolation, networking, and credentials](/docs/introduction/concepts/isolation-networking-credentials).

When the workload only needs to *use* a key in request headers, bind it as a credential instead of injecting it. The guest receives a placeholder in the variable; the host substitutes the real value only on HTTPS requests to the listed hosts, read from the `[secrets]` reference of the same variable name or from the host environment.

```toml
[[network.credentials]]
name = "notion"
environment_variable = "NOTION_API_KEY"
allowed_hosts = ["api.notion.com"]
```

Each binding lists exact hosts or wildcards such as `*.example.com`, and `injection_location = { body = true }` also substitutes the value in a form-encoded request body, for OAuth refresh grants. When `[network].allow_hosts` is set, every credential host must fall under it.

## Service metadata

`[service]` describes the port a deployed service listens on inside the VM.

| Field | Type | Description |
| --- | --- | --- |
| `port` | integer | Guest listening port. |
| `listen` | integer | A separate field, not resolved into `port`. Setting only `listen` leaves `port` unset. |
| `protocol` | string | Free text. `"http"` and `"tcp"` are the intended values, but the value is not checked. |

Only the key names in this table are constrained. A misspelled key such as
`protocl` fails the parse, while a misspelled value such as `"htpp"` does not.

## Configuration precedence

When the same setting is supplied in multiple places, command-line flags take precedence over the Smolfile. The development profile extends the top-level environment, mounts, ports, and initialization commands.

For workload commands, the precedence is:

1. Command arguments after `--` (replaces the Smolfile `entrypoint` and `cmd`)
2. `entrypoint`/`cmd` in the Smolfile
3. Image metadata

For resources, CLI flags override Smolfile values, which override defaults. `[artifact]` values apply when creating a packed artifact, while `[dev]` values apply to local development workflows.

## Configuration versus machine state

A Smolfile describes how to create, start, and connect to a machine. It is not the machine's persistent disk or RAM. To capture prepared disk state for reuse, create a [`.smolmachine` pack](/docs/introduction/concepts/packs-and-smolmachine).
