SpuntoSpunto/@spunto/build

Names and paths

Every container, volume, network, image and file name in ./naming, with its real value, and why changing one renames things that are already running.

A worker is a container called mp-worker-<id>, with volumes and a network named after it, running an image called mp-proj-<slug>:v<n>, and writing its progress to a file at a fixed path. None of that is an implementation detail. Each name is a contract between the shell script or Docker call that creates it, the Docker client that looks it up later, and the UI that shows it.

Before this module, none of them had a single definition. mp-worker-<id> was rebuilt by hand in fourteen places across two apps of Spunto Cloud, and Spunto Lite rebuilt the same names again on its side. A typo in one of those places does not fail to compile. It produces a container that nobody can find at runtime. Module reference: naming.

Docker objects

ExportKindValueExample
workerContainerName(workerId)functionmp-worker-${workerId}mp-worker-abc123
workerNetworkName(workerId)functionmp-worker-${workerId}-netmp-worker-abc123-net
workerVolumeName(workerId, kind)functionmp-worker-${workerId}-${kind}mp-worker-abc123-workspace
workerVolumePrefix(workerId)functionmp-worker-${workerId}-mp-worker-abc123-
WORKER_VOLUME_KINDSconstant["workspace", "home", "docker", "containerd"]
projectImageRef(projectId, version)functionmp-proj-${slug}:v${version}mp-proj-my-app:v7
isProjectImageRef(ref)functiontests /^mp-proj-[a-z0-9-]+:v\d+$/true for mp-proj-p:v9
SHARED_NETWORKconstantmp-shared-net

Worker container, network and volumes

  • The container is the worker.
  • The network is a per-worker bridge. It isolates each worker at L3, so that no worker can reach another worker's container IP directly and bypass the proxy.
  • The volumes. Spawning creates workspace (the code), plus docker and containerd when the project runs Docker-in-Docker. home is historical: nothing creates it any more, but workers created before it was dropped still have one, so teardown has to keep looking for it. This is the "formats, not preferences" rule in action: a value that is no longer written still has to be read.
  • The prefix. Teardown and disk accounting both scan volumes by prefix rather than by listing kinds, so a volume kind added later is still found by code written before it. The trailing - is load-bearing. Without it, mp-worker-abc would match mp-worker-abcdef-workspace, and tearing down one worker would delete another's disk. The container name is also a strict prefix of its volumes (workerVolumePrefix(x) === workerContainerName(x) + "-"), which the agent's lookups rely on.

Project image

export function projectImageRef(projectId: string, version: number): string {
  const slug = projectId
    .toLowerCase()
    .replace(/[^a-z0-9]/g, "-")
    .replace(/-+/g, "-")
    .replace(/^-|-$/g, "")
  return `mp-proj-${slug}:v${version}`
}

An image is keyed by the project's version. The slug is derived rather than stored, because an image reference may only hold lowercase alphanumerics and separators, and a project id is not guaranteed to:

projectImageRef("proj123", 4)       // "mp-proj-proj123:v4"
projectImageRef("Proj_ID.42", 1)    // "mp-proj-proj-id-42:v1"
projectImageRef("--weird--id--", 2) // "mp-proj-weird-id:v2"

isProjectImageRef tells an image this platform built apart from a base image it pulled. It is false for node:24, mp-proj-x and mp-proj-x:latest.

The image recipe version is not part of this name. Spunto Cloud records IMAGE_RECIPE_VERSION on each build row and compares it at spawn. Spunto Lite looks an image up by asking Docker whether the tag exists, so it appends the recipe to the tag itself, composing on top of this function: ${projectImageRef(id, v)}-r${IMAGE_RECIPE_VERSION}. isProjectImageRef does not match those suffixed refs, so Lite does not use it to recognize them. Folding the recipe into the package's name would rename every existing image, which is exactly the kind of change this page is about.

Shared network

SHARED_NETWORK = "mp-shared-net" is the network that every shared service joins under its slug and that every worker joins at spawn, so that a workspace reaches http://<slug>:<port> by DNS. These are Spunto Lite's "shared services", the local counterpart of the Ship pillar.

Paths inside a worker or its image

ExportValueWhereWhat it is
WORKER_STATUS_FILE.mp-status.jsonrelative to the user's homeThe setup status the worker writes
workerStatusPath(home)${home}/.mp-status.json (trailing slashes on home stripped)containerAbsolute path of the above, e.g. /home/vscode/.mp-status.json
EXTENSIONS_DIR/opt/mp-extensionsimageWhere the image build installs VS Code extensions (code-server --extensions-dir)
FEATURE_ENTRYPOINTS_FILE/etc/mp-feature-entrypointsimageA file, not a directory. It is a newline-separated, sort -u'd list of commands to run at container start.
FEATURE_ENTRYPOINTS_STAGING_FILE/tmp/mp-feature-entrypointsimage buildWhere entrypoints accumulate during the build, before being merged into the file above
EXTENSION_FAILURES_FILE/etc/mp-extension-failuresimageExtensions the build could not install, one id per line (see Extension failures)
  • EXTENSIONS_DIR is in the image, at a system path, not in a home. A home is per worker, and putting extensions there would make every worker download them again. At start, the generated script copies them into the user's code-server extensions directory (cp -rn /opt/mp-extensions/. ~/.local/share/code-server/extensions/).
  • Feature entrypoints implement the devcontainer entrypoint contract on Spunto's side. A feature declares one, its install appends it to the staging file (each feature install runs in its own layer and appends blindly), the finalize block of the build appends the staging file to FEATURE_ENTRYPOINTS_FILE and deduplicates with sort -u, and every boot reads the file back and runs each line. Appending rather than overwriting keeps an entrypoint already baked into the base image alongside the new ones.

The README and docs/shared-packages.md still mention FEATURE_ENTRYPOINTS_DIR. That name was _DIR in 0.2.0, which was wrong, and it was corrected to FEATURE_ENTRYPOINTS_FILE "before anything depended on it". The export is FEATURE_ENTRYPOINTS_FILE, and there is no _DIR export.

These are not in ./naming, but they are names that a worker and a control plane share in the same way:

ExportModuleValue
SKILLS_DIR./script.spunto/skills (relative to the home). See Skills layout.
TERMINAL_DIR./terminal/tmp/spunto-term. See the terminal module.
PROJECT_SPEC_KIND./specspunto/project. See Project spec.

Why changing a value renames live things

Every value on this page is baked into something that already exists: a running container, a volume full of someone's code, an image on a node's disk, a file inside an image built last month. Docker does not rename them when the package changes. A live worker keeps the name it was created with.

So if workerContainerName changed, the next deploy of the control plane would look for <new>-abc123, find nothing, and conclude that the worker had vanished. Teardown would leave its volumes behind. Disk accounting would stop counting them. Nothing would fail to compile, and nothing would throw.

These are formats, not preferences. Read any diff of naming/index.ts as a migration, not a rename. The tests pin the exact strings for that reason. A change needs a plan for the objects that already carry the old name: read both names, migrate the objects, or wait until none remain.

The mp- prefix is historical (the product used to be called something else), and it is deliberately left alone for this reason. It is baked into containers, volumes and images that are running right now.

Edit on GitHub

On this page