MUST be invoked before any work involving: layer authoring, layer.yml, tasks, pixi.toml, package.json, Cargo.toml, or any file under layers/. This skill is the authoritative reference for the `tasks:` verb catalog, `vars:` substitution, execution order, and per-verb validation. Every other skill defers here for install-schema questions.
A layer is a directory under layers/<name>/ that installs a single concern. Layers are the building blocks of container images in overthink. Each layer declares its packages, environment variables, services, volumes, and install tasks in a single layer.yml file.
Since the tasks: refactor, there is one YAML file per layer for install logic — no separate Taskfiles, no tasks: / tasks:. Everything an author needs to install flows through tasks: and auto-detected package manifests (pixi.toml, package.json, Cargo.toml).
| Action | Command | Description |
|---|---|---|
| Scaffold new layer | ov image new layer <name> |
Create layer directory with starter layer.yml |
| List all layers | ov image list layers | Show available layers from filesystem |
| List services | ov image list services | Layers with service in layer.yml |
| List volumes | ov image list volumes | Layers with volumes in layer.yml |
| List aliases | ov image list aliases | Layers with aliases in layer.yml |
| Validate | ov image validate | Check all layers and images |
A layer directory can contain any combination of these:
| Artifact | Runs as | Purpose |
|---|---|---|
layer.yml rpm:/deb:/pac:/aur: sections | root | System packages declared declaratively |
layer.yml tasks: list | per-task user: | Ordered install operations — the primary extension point (see catalog below) |
pixi.toml / pyproject.toml / environment.yml | user (builder stage) | Python/conda packages. Multi-stage build. Only one per layer |
package.json | user (builder stage) | npm packages — installed globally via npm install -g |
Cargo.toml + src/ | user (builder stage) | Rust crate — built via cargo install --path |
build.sh | user (builder stage) | Optional post-install script for pixi layers. Runs in the pixi builder after pixi install. For build-time logic that can't be expressed in pixi.toml (C extension compilation, npm builds, binary patching). |
Auto-detection: The build system scans each layer directory for these files. pixi.toml, pyproject.toml, environment.yml, package.json, and Cargo.toml trigger automatic multi-stage builds — no manual install commands needed. Use tasks: only for things those manifests can't express (binary downloads, file copies from /ctx, inline config writes, post-install configuration, go install, etc.).
Root vs user rule: Pixi/npm/cargo builders always run as user — never as root. For tasks:, the user: field per task is explicit: user: root for system-wide changes, user: ${USER} for anything under ${HOME}, or a literal username for custom users (must be created earlier in the same layer via a cmd: task).
Every task in tasks: is a YAML map with exactly one verb key (the discriminator) plus optional sibling modifiers. The verb's value is the primary argument. ov image validate rejects tasks with zero verbs or multiple verbs.
| Verb | Value | Required modifiers | Optional modifiers | Purpose |
|---|---|---|---|---|
cmd: | shell command (multi-line OK) | — | user, comment | Arbitrary shell — last-resort escape hatch |
mkdir: | directory path | — | user, mode, comment | Create directory (mkdir -p; coalesces with adjacent) |
copy: | source relative to layer dir | to: | user, mode, comment | COPY --from=<layer-stage> --chmod= [--chown=] — no RUN |
write: | destination path | content: | user, mode, comment | Write inline content — staged + COPY, no shell heredoc |
link: | symlink path (where the link lives) | target: | user, comment | ln -sf <target> <link> (coalesces with adjacent) |
download: | URL | — (to: unless extract: sh) | user, extract, to, include, mode, env, comment | curl + optional extract (tar.gz/tar.xz/tar.zst/zip/none/sh) |
setcap: | file path | — | user (implicit root), caps, comment | File capabilities (setcap -r strip if caps empty) |
build: | "all" | — | user (default ${USER}), comment | Run auto-detected builders (pixi/npm/cargo/aur) at this point (instead of end-of-layer) |
| Modifier | Applies to | Default | Purpose |
|---|---|---|---|
user | all verbs | root | root / ${USER} / literal username / <uid>:<gid> |
mode | mkdir, copy, write, download | type-specific (0755/0644) | Octal permissions |
to | copy, download | — | Destination in container |
target | link | — | What the symlink points to |
content | write | — | Inline file body (YAML block scalar) |
extract | download | none | Archive format — tar.gz / tar.xz / tar.zst / zip / none / sh |
include | download | — | Extract only these paths (archive formats) |
env | download | — | Env vars for install scripts (sh extract) |
caps | setcap | empty (= strip) | Capability spec (e.g. cap_setuid=ep) |
comment | all | — | Emitted as a Containerfile comment above the task |
# Arbitrary shell
- cmd: dnf install -y https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm
user: root
# Multi-line shell (shared shell — cd persists)
- cmd: |
git clone --depth 1 https://github.com/foo/bar /tmp/src
cd /tmp/src
cargo build --release
install -m 755 target/release/bar /usr/local/bin/bar
rm -rf /tmp/src
user: root
# Make directory
- mkdir: /etc/traefik/dynamic
user: root
mode: "0755"
- mkdir: "${HOME}/.config/sway"
user: "${USER}"
# Copy existing file from layer dir to container (no RUN, pure COPY)
- copy: traefik.yml
to: /etc/traefik/traefik.yml
mode: "0644"
user: root
# Write new file from inline YAML content (no shell heredoc!)
- write: /etc/X11/xorg.conf
mode: "0644"
user: root
content: |
Section "ServerFlags"
Option "DontVTSwitch" "true"
EndSection
# Create symlink
- link: /usr/local/bin/node
target: /usr/bin/node-24
user: root
# Download + extract
- download: "https://github.com/traefik/traefik/releases/download/${TRAEFIK_VERSION}/traefik_${TRAEFIK_VERSION}_linux_${ARCH}.tar.gz"
extract: tar.gz
to: /usr/local/bin
include: [traefik]
user: root
# Install script piped into shell
- download: "https://astral.sh/uv/install.sh"
extract: sh
env:
UV_INSTALL_DIR: /usr/local/bin
user: root
# File capabilities
- setcap: /usr/bin/sway # no caps → strip (setcap -r)
- setcap: /usr/bin/newuidmap
caps: "cap_setuid=ep"
# Explicit builder placement (lets you run tasks AFTER pixi/npm/cargo)
- build: all
user: "${USER}"
- cmd: pip install --no-deps ./vllm-nightly.whl
user: "${USER}"
copy: vs write: — never conflateThey happen to emit COPY directives under the hood, but they have entirely distinct semantics:
copy | write | |
|---|---|---|
| Source of bytes | File on disk under layers/<name>/ | Inline content: block in layer.yml |
| Replaces old pattern | cp /ctx/foo bar + chmod | cat > foo << 'EOF' … EOF |
| Validation check | src must exist under layer dir at generate time | content must be non-empty |
| Cache key | Layer-stage file content | Content-addressed staged file at .build/<image>/_inline/<layer>/<sha256> |
| Shell-heredoc involvement | None | None — content never appears in the Containerfile |
write: is the heredoc-safe path: the YAML content: is staged to disk at generate time and delivered via COPY. That means $, backticks, nested EOF markers, and arbitrary binary-safe text are all handled without any escaping. If you find yourself writing cat > foo << 'EOF' inside a cmd: block, use write: instead.
vars: and ${VAR} Substitutionvars: is a layer-local map[string]string. Values are emitted as ENV before the layer's tasks, so every subsequent directive in the layer (including COPY --chmod= paths) sees them as shell-resolvable ${VAR} references.