How My Remote Dev Setup Evolved: From VS Code Containers to mosh, tmosh, dcon, and Now Zellij

How My Remote Dev Setup Evolved: From VS Code Containers to mosh, tmosh, dcon, and Now Zellij

I didn’t sit down and design my remote development setup. It evolved — one papercut at a time, with plenty of wrong turns, redundancy, and dead ends along the way.

I want to tell that story honestly, because the tidy “here is my perfect stack” version would be a lie. The truth is messier and, I think, more useful: each piece exists because something before it hurt, and some of the pieces now overlap in ways I haven’t cleaned up yet. It’s an ongoing reflection, not a finished architecture.

Here’s the whole winding path — how I got from “VS Code with containers on my laptop” to “mosh → tmosh → docker sh → Zellij,” and the open questions I’m still chewing on.

The Thing I Was Chasing the Whole Time

Before the journey, the constant. Every step was in service of one goal:

I want to close my laptop, open it somewhere else on a different network, reconnect, and be back at work — with as little cleanup as possible.

I couldn’t just leave an environment running on a dev server and trust I’d walk right back into it. For a long time, coming back was always work. Every tool below is a step toward making that work disappear.

Where It Started: VS Code and Containers, on My Laptop

The beginning was comfortable and local: VS Code with containers, then VS Code with dev containers. My project’s tools, runtimes, and dependencies lived inside a container instead of on my host machine. Reproducible, isolated, clean — I’ve written more about why I containerize the whole toolbelt, so I won’t relitigate it here.

This was great. But it was all running on my machine.

The First Move: Push the Containers to a Dev Server

The natural next step was to stop running those containers on my laptop and move them to a dedicated dev server, driven with remote VS Code — the editor on my laptop, the containers and heavy lifting on the server.

And it worked. But it was heavy. Remote VS Code carries a lot of weight, and — crucially — it leaned on plain SSH, not mosh. Every time my connection blinked, that fragile pipe broke. For a laptop that roams between networks and sleeps constantly, plain SSH is a dozen little deaths a day.

That weight and that fragility are what pushed me toward the terminal.

The Turn to the Terminal: Light, and tmux So Nothing Dies

The terminal is light. That was the whole appeal. No heavy remote editor process, just a shell on the server.

But a bare shell has the opposite problem from VS Code: close the connection and everything running in it dies. So tmux was the obvious companion — it keeps sessions alive and detached on the server. Builds keep building, servers keep serving, whether or not I’m connected.

Now I was on the server, in the terminal, with tmux keeping my work alive. But my code didn’t live on the server’s host — it lived inside a dev container. Every time I wanted to actually work, I had to find the container, docker exec into it with the right user and shell, and land in the right directory. Repetitive, fiddly, every single time.

dcon Is Born — Logically

So I wrote dcon, part of my remote-code-toolbox. It came out of that friction directly and logically: seamlessly connect me to a project’s dev container via tmux. Each project gets its own persistent tmux session with shells running directly inside the container. It even derives a sane session name from the project path.

dcon                           # Connect to this project's container session
dcon -n api                    # A named window
dcon --stack 3                 # Three vertically-stacked panes
dcon -n api --side-by-side 2   # Two side-by-side panes in an "api" window

Same one-liner install philosophy as the rest of my tools:

curl -fsSL https://raw.githubusercontent.com/totophe/remote-code-toolbox/main/scripts/install.sh | sh

Note the timeline here, because it matters later: dcon came first. It was doing the tmux → docker sh hop, wrapping the whole “get me into the right container session” dance into one command. Remember that — it’s the source of the redundancy I’ll get to.

The Terminal-Emulator Detour: Terminal.app → iTerm → cmux

Meanwhile, on the laptop side, I was fighting a parallel battle nobody warns you about: the terminal emulator itself.

macOS’s built-in Terminal.app isn’t really up to serious tmux work — I kept hitting incompatibilities. So iTerm was the way; it plays far better with tmux. But then I had a new problem: managing multiple iTerm windows was a nightmare on disconnection. Reconnect, and I’m staring at a pile of dead panes trying to reconstruct what went where.

cmux came to the rescue and, honestly, worked well — especially once I paired it with mosh, the mobile shell that survives IP changes and reconnects transparently when the network comes back. mosh is what finally made the transport resilient. Between cmux and mosh, disconnection stopped being catastrophic.

But it still wasn’t perfect. There were rough edges, and even with cmux and iTerm, copy/paste was sometimes tricky — the kind of small, persistent annoyance that never quite rises to “fix it today” but grinds on you for months.

tmosh: Born From a Semi-Joke at a Conference

Then came a conference, and tmosh — which started as half a joke, half a challenge, and accidentally became my favorite little tool.

The setup that day was the usual: mosh in so the session survives the drops, then tmux so nothing dies. Which meant every login was the same dance — tmux ls, squint at the list, attach to the right detached session or start a new one. Every. Single. Time. Someone said “this is a waste of 15 seconds, fifty times a day,” and a few hours later tmosh existed: a tiny Rust binary that greets you the instant you log in and just asks — attach, create new, or drop to a plain shell.

It runs automatically on interactive login, guarded so it never interferes with scp/rsync/scripts, and it execs straight into tmux with no wrapper left behind. I told the full origin story in its own article — and against all expectation, this throwaway conference build became the thing I now reach for on every single login.

The Honest Part: Now I’m Doing the Job Twice

Here’s where the tidy-stack narrative falls apart, and where I have to be honest.

dcon came before tmosh. dcon already knew how to get me from tmux into the right docker sh session. Then tmosh arrived and took over the front door — the “greet me and pick a session” job. And these two now overlap. In practice, using tmosh and dcon together, I’m partly doing the same job twice: two tools, both in the business of “get me into the right session,” stitched together rather than designed together.

It works. But it’s redundant, and I know it.

And I Still Have the Workspace Problem

On top of the redundancy, the thing I most wanted was never really solved: remote workspaces.

Once I’m inside the container, I want a real per-project layout — editor pane, dev server pane, test watcher, logs, a shell — laid out the same way every time, standing itself up on command. For a long time that meant raw tmux plus brittle shell scripts and tmux send-keys incantations. It was the ugliest, most fragile part of the whole thing.

So the real state of affairs, until recently, was: an evolved-not-designed chain with a redundant middle and an unsolved end.

Enter Zellij — My Current Experiment

That’s what led me to Zellij, which I’m testing right now. I want to be clear it is an experiment — there are other alternatives on the market, Zellij just happens to be my first try. But it’s a very good one, and it points squarely at the workspace problem.

Zellij is a modern terminal workspace — a multiplexer in the tmux family, but designed around how I actually want to work:

  • Declarative layouts. Instead of shell scripts firing send-keys, you describe your workspace in a layout file using KDL, a clean human-friendly config language. Tabs, panes, split directions, and the command each pane runs on startup — all in one readable file you can commit alongside the code.
  • Discoverability. Keybindings are visible and modal rather than a wall of memorized Ctrl-b chords.
  • Modern conveniences — floating panes, real tabs, sessions — usable out of the box.

A layout can be as small as this:

layout {
    pane
    pane split_direction="vertical" {
        pane
        pane command="htop"
    }
}

But the payoff is a per-project layout that stands up my whole environment in one shot — editor, dev server, test watcher, logs — each pane launching its own command. Commit that file with the repo, and the workspace materializes identically every time. It’s the same principle that made dev containers click, finally applied to the layout of my work and not just its tools.

With Zellij in the mix, my current flow is:

mosh  →  tmosh  →  docker sh  →  zellij

Where This Leaves Me — and the Open Questions

So this is genuinely a work in progress, not a victory lap. A few things are still unresolved, and I’d rather write them down than pretend they’re solved:

The dcon/tmosh redundancy. The obvious fix is a lighter dcon — or a merge of tmosh and dcon into one tool where I can also declare my workspaces. But that cuts against the thing I like most about both: they’re tiny, single-purpose, single static binaries. Combining them, and adding workspace declaration on top, would make each one heavier and less sharp. So it’s an ongoing reflection — I don’t yet know whether “one slightly heavier tool” beats “two light tools that overlap.”

The missing automation step. Right now the docker sh → zellij hop is manual, exactly the way tmux → docker sh used to be manual before dcon existed. The logical move is a new dcon that automates docker sh → zellij the way the old one automated tmux → docker sh. That would collapse the flow toward a single reflex: connect, and be working. Whether that’s a new dcon, a lighter dcon, or the tmosh/dcon merger above — that’s the decision I’m sitting with.

That’s the setup, as it actually is today: evolved rather than designed, a little redundant in the middle, with the workspace layer finally taking shape through Zellij and one automation step still to write. If you’re pushing your own development onto a remote box, maybe the honest version of this journey — dead ends included — saves you a few of the papercuts it cost me.

About the Author
totophe's avatar

totophe

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