devcon: The New dcon I Promised — Dev Containers Without VS Code

devcon: The New dcon I Promised — Dev Containers Without VS Code

Yesterday I published the honest, winding story of my remote dev setup — how it evolved one papercut at a time from “VS Code with containers” all the way to “mosh → tmosh → docker sh → zellij.” And I ended it on two unresolved problems I was genuinely sitting with:

  1. The dcon/tmosh redundancy — dcon came first and did the tmux → docker sh hop; then tmosh took over the “greet me and pick a session” job, and now the two overlapped.
  2. The missing automation — the docker sh → zellij step was still manual, exactly the way tmux → docker sh used to be before dcon existed. The logical move, I wrote, was “a new dcon that automates docker sh → zellij the way the old one automated tmux → docker sh.”

I ended that article with “maybe tomorrow, hahaha.” Reader, it was tomorrow.

Meet devcon: a from-scratch rewrite that brings up a project’s dev container and drops you into its shell — from the command line, without VS Code — and that was designed from the first commit to resolve exactly those two tensions.

The One-Line Pitch

Bring up a project’s dev container and drop into its shell — from the command line, without VS Code.

That’s it. No VS Code, no Node, no @devcontainers/cli. A single static Rust binary that does what “Open in Container” does in the IDE, except it happens in your terminal over SSH/mosh, where my whole workflow already lives.

What It Actually Does

Run devcon in a project directory, and it walks the full lifecycle that VS Code normally hides from you:

  1. Finds .devcontainer/ by traversing up from your current directory
  2. Parses devcontainer.json — including JSONC with comments and trailing commas, because real config files have those
  3. Locates the running container via the devcontainer.local_folder label
  4. Starts the stack if it’s downdocker compose up -d or docker run, prompting first (or -y to skip)
  5. Runs postCreateCommand once per container creation
  6. Runs postStartCommand once per container start
  7. Resolves the workspace directory inside the container
  8. Figures out the right shell and execs you straight into it
devcon                     # start the stack and drop into the container shell
devcon -y                  # skip the "start it?" confirmation
devcon --shell /bin/bash   # override the shell for this session
devcon self-update         # update to the latest release

Install is the same one-liner reflex as the rest of my tools:

curl -fsSL https://raw.githubusercontent.com/totophe/devcon/main/install.sh | sh

The VS Code You Don’t Notice: Lifecycle Hooks

Here’s the part I underestimated until I built it. When you “Open in Container” in VS Code, the IDE quietly orchestrates a whole lifecycle — and if you leave VS Code behind, you inherit that job.

The two that matter most are postCreateCommand and postStartCommand, and getting their semantics right is surprisingly fiddly:

  • postCreateCommand must run once per container creation — install dependencies, set up the workspace — and never again on reconnection. devcon tracks this with a dev.devcon.postcreate label plus a /tmp/.devcon-postcreate-done sentinel, so it survives restarts without re-running.
  • postStartCommand must run once per container start — the start-time setup a service needs — and, critically, it has to run again after a docker restart but not on a plain reconnection to an already-running container. devcon keys this to the container’s State.StartedAt timestamp, so a restart (new timestamp) re-triggers it while reconnecting doesn’t.

Both run as the declared remoteUser, in the workspace directory, exactly as the IDE would do them. This is the unglamorous machinery that makes “terminal-first dev containers” actually work instead of subtly breaking the first time a container needs setup. It’s the part that justified a rewrite rather than a patch on the old dcon.

But Wait — Why Not Just Use @devcontainers/cli?

This is the fair question, and I want to answer it head-on: there is an official CLI, @devcontainers/cli. It reads devcontainer.json, it has up, exec, build, and run-user-commands. So why write my own?

Three reasons, and none of them is “the official one is bad” — it’s genuinely good at what it’s for.

1. It’s a reference implementation, not an ergonomic front door. The official CLI is explicitly the reference implementation of the Dev Container spec — a building block that IDEs and tools compose. It gives you the primitives (up, then exec, then run-user-commands), but it doesn’t wrap them into the one motion I actually want: find the running container for this project, bring the stack up if it’s down, run the right hooks exactly once, resolve the workspace dir, pick the shell, and drop me in. That orchestration is precisely the job VS Code does on top of the CLI — and precisely the job I needed something to do when VS Code isn’t in the loop. devcon is that thin orchestration layer.

2. exec runs a command; it doesn’t drop you into a shell. devcontainer exec runs an individual command with the container’s environment and user applied — it’s built for one-shot invocations, not “put me in a persistent interactive zsh in the workspace directory and get out of my way.” I could script an interactive shell on top of it, but that scripting is most of what devcon does — and doing it in a single static binary is cleaner than a shell wrapper around a Node CLI.

3. The dependency weight is exactly what I’m trying to escape. The official CLI is an npm package that pulls in the Node ecosystem — and building its dependencies wants Python and a C/C++ toolchain. My entire remote-first journey has been about shedding weight on the dev server: that’s why I left remote VS Code for the terminal in the first place. Installing a Node toolchain on every box just to enter a container runs directly against that grain. devcon is one static Rust binary with a one-line install and no runtime — it drops onto a fresh server and works.

So it’s not a rejection of the spec — devcon reads the same devcontainer.json and honors the same hooks. It’s a different tool for a different seat: the official CLI is the engine tools build on; devcon is the terminal-native driver I wanted for a VS Code-free, remote-first workflow. And staying off the Node ecosystem is a feature, not an oversight.

How devcon Answers Question #1: It Refuses to Touch the Multiplexer

This is the design decision I’m happiest about, and it directly dissolves the redundancy I complained about yesterday.

The old dcon created its own tmux session as part of getting you into the container. That’s precisely what started overlapping with tmosh once tmosh took over session management. devcon makes the opposite choice, on purpose:

devcon deliberately does not touch the multiplexer. It never starts its own tmux session, so there’s no tmux-in-tmux.

It assumes you’re already inside whatever session your login process set up — which, in my world, is exactly what tmosh puts me in the moment I connect. devcon just ensures the container is alive and execs into it, then gets out of the way. No wrapper process, no nested multiplexer, no second tool fighting tmosh for the session.

So the layering finally stops overlapping:

  • tmosh owns the session — it greets me on login and puts me in the right place.
  • devcon owns the container — it brings the stack up and drops me inside.

Two sharp, single-purpose tools that compose instead of duplicate. That’s the redundancy, resolved — not by merging them into one heavier tool (the option I was wary of yesterday), but by giving each a cleaner boundary.

How devcon Lines Up Question #2: Zellij, Parked but Wired

The second open question was the docker sh → zellij automation. devcon doesn’t fully close it yet — but it’s honest about that, and it has already built the machinery.

The roadmap entry is a --workspace flag that would exec zellij attach -c <codename> instead of dropping you into a bare shell — literally completing the tmux → docker → zellij vision from yesterday’s article. Its status, in the README’s own words:

Parked; the machinery is already in place (it only swaps the final exec command).

I like that this is stated plainly rather than oversold. devcon’s final step is already “exec something into the container”; turning that something from $SHELL into zellij attach -c <codename> is a small, well-defined swap. The codename it would attach to is the same project-derived name devcon already computes (workspaces/totophe/devcon → totophe_devcon), so a per-project Zellij workspace would come up under a stable, predictable name every time.

When I flip that flag on, the flow from yesterday collapses into a single reflex:

mosh  →  tmosh  →  devcon --workspace
                     (brings up the container AND attaches its Zellij workspace)

Connect, and be working — which was the dream I ended yesterday’s article on.

The Naming Carries Over

One small continuity detail I kept deliberately: devcon derives the container codename from the project path exactly the way dcon did.

/home/user/workspaces/totophe/devcon  →  totophe_devcon
/home/user/workspaces/myproject       →  myproject
/home/user/projects/myapp             →  myapp

If a repo sits two levels deep inside a workspaces folder, the parent name gets prefixed. It’s a tiny thing, but it means my sessions and (soon) my Zellij workspaces all share one consistent, path-derived vocabulary across the whole toolchain.

Why a Rewrite Instead of Patching dcon

Yesterday I laid out the tension explicitly: a lighter dcon, or a tmosh/dcon merge that could declare workspaces — but merging cut against the thing I liked most, that both tools were tiny and single-purpose.

devcon is my answer, and it’s the un-merge rather than the merge. Instead of one heavier tool that does sessions and containers and workspaces, I drew a sharper line: devcon owns the container lifecycle — properly, including the hooks VS Code used to run — and pointedly stays out of the session business that tmosh already owns. It’s a rewrite because the hook-lifecycle machinery and the “never touch the multiplexer” stance are foundational, not bolt-on; they’re easier to build cleanly from scratch than to retrofit onto the tmux-coupled dcon.

Where This Leaves the Setup

So one day later, the map looks meaningfully cleaner than the one I drew yesterday:

  • mosh — resilient transport.
  • tmosh — greets me on login, owns the session.
  • devcon — brings up the dev container and drops me in, VS Code-free, hooks and all.
  • Zellij — the per-project workspace, soon to be attached automatically by devcon --workspace.

The redundancy I confessed to yesterday is gone by design. The one manual step that remains — attaching Zellij — is parked behind a flag whose machinery already exists. It’s still a work in progress, and I’m still testing Zellij rather than declaring victory. But for the first time, every piece has a clean boundary and there’s exactly one obvious thing left to wire up.

Turns out “maybe tomorrow” was the right estimate.


🍴 Fork it on GitHub

Want to try devcon or contribute to the project?

Star the repository if you find it useful!
🐛 Report issues or suggest new features
🤝 Contribute — especially if you want to help un-park the Zellij workspace mode

About the Author
totophe's avatar

totophe

Creative Mind, Digital Development Strategist, and Web & Marketing Technologies Consultant in Brussels, Belgium