Devcontainers
Run a repository's own devcontainer.json on a workstation, without rewriting it as a WorkstationConfig.
Many repositories already describe their development environment in
devcontainer.json, the format VS Code, GitHub Codespaces, and most other
dev-environment tools read. Ringleader can run that file directly: attach the
repository to a workstation as a source, set one flag, and the workstation clones
the code, builds the devcontainer, and puts your shell inside it.
The point is that you do not have to translate an environment your team already
maintains into a WorkstationConfig.
The file runs under the reference devcontainer CLI, exactly as upstream defines
it, and Ringleader adds what the format has no words for: the machine itself, its
lifecycle, its placement, and who may reach it.
Declaring one
A devcontainer is a property of a checkout, so it is declared on the source that carries the repository:
apiVersion: workstations.ringleader.dev/v1
kind: Workstation
metadata:
name: app-box
namespace: dev
spec:
sources:
- name: app
path: /workspaces/app
git:
url: git@github.com:acme/app.git
devcontainer:
enabled: true
providerConfig:
cpus: 4
memory: 8 # GiB; see Sizing below| Field | Type | Description |
|---|---|---|
devcontainer.enabled | bool | Run this source’s devcontainer on the workstation. Exactly one source may set it; a second is refused at apply. |
devcontainer.configPath | string | Optional path to the devcontainer file, relative to the source’s path. Omit it for the CLI’s own discovery: .devcontainer/devcontainer.json, then .devcontainer.json. |
Three things follow from the declaration:
- The source’s
pathis the workspace folder. The checkout is mounted into the container there, exactly as the format expects. A source with nopathdeclares nothing usable and is refused. - The
dockeranddevcontainer-clidevtools are implied. You do not have to declare them; declare one yourself only to pin its version. - A managed service named
devcontaineris synthesized. It runsdevcontainer upwith a readiness probe, and with a start timeout sized for a real image build rather than systemd’s 90-second default.
Where your shell lands
The workstation is a VM, and the devcontainer runs inside it on Docker. Your tools land in the container by default, because that is the environment you asked for:
| You run | It lands |
|---|---|
rl shell, ssh <box>, VS Code Remote SSH, JetBrains Gateway | inside the container, as the file’s remoteUser |
rl shell --script, rl file cp | inside the container |
rl shell --host, rl file cp --host | on the VM |
scp, sftp | on the VM (see below) |
Two deliberate ways to reach the VM itself:
- The escape account. Every devcontainer workstation gets a second login user,
<user>-host(so login userdevgetsdev-host), whose shell is an ordinary VM shell and which accepts the same SSH keys. It exists so a workstation whose container failed to build is still reachable and repairable, and it is the way to point an editor at the VM rather than at the container. --host, or arun-on-hostprefix.rl shell <box> --host -- <command>andrl file cp --hostrun on the VM. With plainssh, prefixing the remote command does the same:ssh <box> run-on-host uptime.
One asymmetry to know about: scp and sftp use SSH’s file subsystem, which does
not go through the login shell, so they land on the VM while rsync and
rl file cp land in the container. This matters less than it sounds for the
workspace itself: the checkout is mounted, so a file under the source’s path is
the same file either way. It is only a real difference for paths outside the
workspace folder.
Readiness
The workstation reaching Configured means devcontainer up succeeded and the
container passed its readiness probe. A devcontainer that fails to build blocks
Configured exactly like any other failed configuration step: the workstation
reports the failing step, rl troubleshoot has its output, and the escape account
still works, so the machine stays administrable while you fix the file.
Expect the first build to take a while. devcontainer up pulls the base image,
builds one layer per Feature, and runs the file’s lifecycle commands, so a fresh
workstation with a heavyweight devcontainer is minutes, not seconds.
Ports
The container’s ports are published to the VM. Reaching them from your own machine
stays an explicit ports / LocalBinding
declaration, exactly as on any other workstation. Nothing inside the container is
auto-forwarded to your laptop, including anything the file lists in
forwardPorts.
Sizing
hostRequirements in the file describes what the container needs, and
Ringleader reads it as a floor on the workstation’s own sizing, never as a
substitute for it: a declared providerConfig wins. Size the VM for the container
plus the image build plus the Docker engine. Disk is the sharp edge, because an
image build plus its layers can be tens of gigabytes and hostRequirements.storage
describes only the workspace.
Users
identity governs the VM: SSH login, sudo, home directory. The file’s remoteUser
governs the container, applied by the reference CLI as upstream specifies. You log
in as identity.user and land in the container as remoteUser; neither is
rewritten to match the other.
Changing the file
A changed devcontainer.json does not rebuild the container by itself. A rebuild
is a large event (a full image build, on a machine you may be working on), so
Ringleader never fires one off a git pull. To pick up a change, restart the
devcontainer service on the VM
(rl shell <box> --host -- sudo systemctl restart devcontainer), or drive the
devcontainer CLI there yourself.
What v1 supports
Because the file is executed by the reference implementation, “supported” means Ringleader does not interfere:
- All three build shapes:
image,build.dockerfile, anddockerComposeFilewithserviceandrunServices. - Features, including their options and install ordering.
- The in-container lifecycle commands (
onCreateCommand,updateContentCommand,postCreateCommand,postStartCommand) andwaitFor. remoteUser,containerUser, andupdateRemoteUserUID.workspaceFolderandworkspaceMount.containerEnvandremoteEnv.mounts,overrideCommand,init,privileged,capAdd,securityOpt, andrunArgs.
What v1 does not support
The format is large, and a page that does not say what is skipped would read as a compatibility promise. These are deliberate, each for a stated reason:
| Not supported | Why |
|---|---|
forwardPorts auto-forwarded to your machine | Container ports are published to the VM; reaching them from a laptop stays an explicit ports / LocalBinding declaration. |
customizations | Tool-specific by definition. customizations.vscode.extensions is the one likely to be revisited. |
initializeCommand | Specified to run on the host, and here the host is the VM rather than your laptop, so running it would honor the letter and break the intent. It is refused loudly with a diagnostic. |
postAttachCommand | rl shell is SSH and has no attach event to hang it on. |
shutdownAction | The workstation’s lifecycle is Ringleader’s (stopped, ttl, teardown). A file in a repository must not decide when a billed VM stops. |
hostRequirements as sizing | Read as a floor on providerConfig and reported when the machine is smaller, never a substitute. See Sizing above. |
| Image and Feature verification | No signature or provenance check is performed on the image, the build.dockerfile base, or any Feature artifact. A deliberate deferral, stated here so it is not assumed. |
A private registry for the file’s own image/build refs | spec.image.pull authenticates the workstation’s image; the devcontainer’s own references have no credential channel yet. |
See also
- Code sources: the
sources[]field this declaration lives on. - Workstation: the machine hosting the container, and its
Configuredcondition. - docker and devcontainer-cli: the two implied devtools.
- LocalBinding: forwarding a published port to your machine.