> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vers.sh/llms.txt
> Use this file to discover all available pages before exploring further.

# Dockerfile support

> How `vers build` turns a literal Dockerfile into a Vers commit, with layer caching.

`vers build` reads a literal Dockerfile and produces a Vers **commit**. Each instruction becomes a cached "layer." The output is the same commit id any other Vers command accepts — it boots with `vers run-commit`, branches with `vers branch`, and can be tagged with `vers tag` or `-t`.

This page explains the model. For the command-level reference, see [`vers build`](/cli-reference/build).

## The mapping

Every supported Dockerfile instruction translates to one or more Vers actions:

| Dockerfile                               | Vers action                                                        |
| ---------------------------------------- | ------------------------------------------------------------------ |
| `FROM scratch`                           | `vm.NewRoot` with explicit sizing flags                            |
| `FROM <tag-or-commit>`                   | `vm.RestoreFromCommit` (tag lookup, then repo ref, then commit id) |
| `RUN`                                    | `vers exec` with accumulated ENV / WORKDIR / USER                  |
| `COPY` / `ADD` (local)                   | SFTP upload from the build context                                 |
| `ENV`, `ARG`, `WORKDIR`, `USER`, `LABEL` | Builder state — no VM mutation, but affects future layers          |
| `CMD`, `ENTRYPOINT`, `EXPOSE`            | Captured as commit metadata                                        |

After each RUN or COPY, the builder commits the VM and moves on with a fresh parent.

## Supported instructions

`FROM`, `RUN`, `COPY`, `ADD` (local paths only), `ENV`, `ARG`, `WORKDIR`, `USER`, `LABEL`, `CMD`, `ENTRYPOINT`, `EXPOSE`.

Line continuations, exec-form arrays (`CMD ["node", "server.js"]`), `.dockerignore` patterns, and `$VAR` / `${VAR}` substitution are supported.

<Warning>
  Not yet supported — parsed but rejected at execution:

  * Multi-stage builds (`FROM ... AS name`, `COPY --from=stage`)
  * `ADD` from URLs or with tar auto-extraction
  * `HEALTHCHECK`, `SHELL`, `VOLUME`, `STOPSIGNAL`, `ONBUILD`
</Warning>

## The cache model

Every materializing instruction (RUN or COPY/ADD) produces a commit. Before executing, the builder computes a **cache key**:

```
sha256(
  parent_commit_id
  ‖ normalized_instruction_text
  ‖ env / workdir / user snapshot
  ‖ content_hash (for COPY)
)
```

If that key has been seen before — stored in `.vers/buildcache.json` in your project — the builder **skips execution** and uses the cached commit as the new parent. Otherwise it runs the step and writes the resulting commit back to the cache.

This is the same shape as Docker's layer cache, with two differences:

1. Layers here are first-class **Vers commits**, not OCI blobs. You can `vers branch` a mid-build layer directly.
2. Cache invalidation is content-based, not timestamp-based. Touching a file without changing it doesn't invalidate.

### Cache invalidation

A layer re-runs when any of:

* The instruction text changes (even whitespace-significant)
* The accumulated ENV/WORKDIR/USER before this instruction differs
* For COPY: any file in the copied tree changes size, mode, or contents
* For COPY: `.dockerignore` changes the visible tree
* `--build-arg` values change (and that ARG is declared in the Dockerfile)

### Cache busting

* `--no-cache` — bypass the cache for this build.
* Delete `.vers/buildcache.json` — reset all cache state.
* `vers commit delete <commit-id>` — remove specific cached layers; next build re-runs them.

### Stale cache

If a cached commit id has been deleted server-side, the builder falls through to real execution and prints a `cache entry stale` note. No hard failure — you just lose the speedup for that step.

## FROM semantics

| Form                      | Behaviour                                                                                                             |
| ------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `FROM scratch`            | Fresh VM via `vm.NewRoot`. Requires `--mem-size`, `--vcpu-count`, `--fs-size-vm-mib`. There are no implicit defaults. |
| `FROM <name>`             | Looked up as a vers tag first, falling back to treating `<name>` as a commit id.                                      |
| `FROM <repo>:<tag>`       | Repo-scoped tag within your organization (e.g. `FROM my-app:v1`).                                                     |
| `FROM <org>/<repo>:<tag>` | Org-local repo first, then cross-org public repo (e.g. `FROM acme/ubuntu:latest`).                                    |
| `FROM <org>/<repo>`       | Shorthand for `<org>/<repo>:latest`.                                                                                  |
| `FROM <commit-id>`        | Restored directly.                                                                                                    |

Vers resolves `FROM` against its own repo and tag namespace, not Docker Hub. References containing `/` are resolved org-local first (since repo names may contain slashes), then fall back to cross-org public repos. Use `FROM org/repo:tag` (or `FROM org/repo` for the `latest` tag) to reference [public repos](/cli-reference/repo) shared by other organizations.

<Note>
  The `org/repo:tag` form only resolves **public** repos cross-org. To reference your **own private** repo, use the bare form `repo:tag` (without the org prefix). Writing `FROM myorg/my-private-repo:v1` will fail because no org-local repo named `myorg/my-private-repo` exists, and the cross-org fallback requires the repo to be public.
</Note>

## A worked example

```dockerfile theme={"theme":{"light":"min-light","dark":"min-dark"}}
FROM scratch
ARG VERSION=dev
ENV APP_VERSION=${VERSION} \
    NODE_ENV=production
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
COPY . /app
EXPOSE 3000
CMD ["node", "server.js"]
```

Building this (`vers build --mem-size 2048 --vcpu-count 2 --fs-size-vm-mib 4096 -t myapp:prod .`) produces:

<Steps>
  <Step title="FROM scratch">
    Fresh VM. No commit yet.
  </Step>

  <Step title="ARG / ENV / WORKDIR">
    Metadata only. Applied to builder state, bakes into every subsequent layer's cache key.
  </Step>

  <Step title="COPY package.json package-lock.json ./">
    Uploaded; layer committed. Cache keyed on the two files' contents.
  </Step>

  <Step title="RUN npm ci --omit=dev">
    Executed inside the VM; layer committed.
  </Step>

  <Step title="COPY . /app">
    Every file (minus `.dockerignore`) uploaded; layer committed. Most likely to invalidate on change, which is why package.json copy came first.
  </Step>

  <Step title="EXPOSE / CMD">
    Metadata. Captured in the build output, no new layer.
  </Step>

  <Step title="Tag">
    Final commit gets tagged `myapp:prod`.
  </Step>
</Steps>

Rebuilding without changing anything is near-instant — every step hits the cache.

## Common patterns

### Dependency layer, then source layer

Put `COPY` of dependency manifests (`package.json`, `go.sum`, `requirements.txt`) **before** `COPY . /app`. That way editing source code doesn't invalidate the dependency install layer.

### Using a golden base

```dockerfile theme={"theme":{"light":"min-light","dark":"min-dark"}}
FROM golden-base:v1     # resolved as a vers tag
RUN ./setup-app.sh
COPY . /app
CMD ["./serve"]
```

### Dockerfile as the project's run config

Instead of maintaining `vers.toml` *and* a `Dockerfile`, you can tag the build and use `vers run-commit` as your default boot path:

```bash theme={"theme":{"light":"min-light","dark":"min-dark"}}
vers build -t myapp:latest .
vers run-commit myapp:latest
```

## See also

* [vers build](/cli-reference/build) — the command reference
* [Commits](/concepts/commits) — what `vers build` produces
* [vers tag](/cli-reference/tag), [vers repo](/cli-reference/repo) — naming and sharing builds
