DEV Community

Cover image for Replacing Node-RED with a 10 MB browser editor and a start.sh
Panth Patel
Panth Patel

Posted on Originally published at panth.vardayinitech.in

Replacing Node-RED with a 10 MB browser editor and a start.sh

A client disconnected our server from the internet for security, and the integration for that client still had to be written on it.

I'm Panth, and I lead the software team at Oizom, where we build Envizom, an air quality platform that many clients run on their own servers. Every one of those deployments needs a bit of custom work: pull data from somewhere, or expose an API in a schema the client gives us for a public display. That work can't go into our production code. This post is about where it lives now: codeshell, a small browser workspace I built and open-sourced under Apache-2.0.

Node-RED first

Node-RED was the default for these integrations before I joined. It is fragile for this kind of work:

  • An async for loop is hard to write.
  • Five API calls with something equivalent to Promise.all is close to rocket science.
  • Debugging is the worst part.
  • If there is a JavaScript library for the job but no Node-RED node in the marketplace, you spend five hours figuring out how to set the library up.

These are short programs. Plain code is the right tool for them.

Then one repo per deployment

The next idea: each deployment gets its own codebase, built into one Docker container that holds all of that server's customisations.

It works until the client disconnects the server from the internet. Then the integration still has to be written, so you open a shell in the container, edit the code with vi, and hope nobody pulls the latest image before you have copied the same code back into the repo.

What I actually needed

A VS Code-like editor in the browser, on the server, with the code directory mounted from outside so it survives a restart. If the code lives in one volume, and the volume is backed up, the integration doesn't need a repo at all.

VS Code in the browser is heavy for this, because the language servers and compilers run on the server. I looked for a light editor and didn't find one, so I wrote codeshell.

What codeshell is

Part What
Backend Go, one ~10 MB static binary with the UI embedded. It serves plain files and PTYs. No language servers, no compilers.
Frontend Svelte 5, CodeMirror 6 for the editor, xterm.js for terminals. Syntax modes load per file type.
/code The one required volume. Root of the file tree, working directory of every terminal.
/code/start.sh Runs on every container start, restart and recreate.
Size about 1,750 lines of Go and 3,700 of Svelte and TypeScript

Running one integration is one container:

docker run -d --name myapp \
  -p 8080:8080 -p 3000:3000 \
  -e CODESHELL_PASSWORD=change-me \
  -v myapp-code:/code -v myapp-state:/var/lib/codeshell \
  ghcr.io/panth977/code-shell:latest
Enter fullscreen mode Exit fullscreen mode

Port 8080 is the editor. Port 3000 is whatever the integration listens on.

start.sh is the deploy

Terminals are temporary on purpose. Anything you install or start from a terminal is gone after a restart. What has to come back goes in start.sh:

#!/usr/bin/env bash
set -e
command -v node >/dev/null || (apt-get update && apt-get install -y nodejs npm)
npm ci
exec node index.js
Enter fullscreen mode Exit fullscreen mode

The image is Debian slim, so start.sh can apt-get install whatever runtime the integration needs. On the first run it installs Node and the modules. On later runs node_modules is already in the volume, and mounting /root and /opt as volumes too keeps package caches and runtimes, so restarts get faster.

codeshell supervises the script. Its output shows as a start.sh entry in the terminal panel, next to your own terminals, with Restart and Stop buttons, and its state is in the top bar: running, stopped, exited, waiting or missing. The restart policy is on-failure by default, and restarts back off from 1 second, doubling, to a cap of 60 seconds. On a fresh volume it writes a commented start.sh template with a Node and a Python example.

Terminals keep their scrollback on the server, so a page reload or a dropped connection doesn't lose them.

Where it sits

The other half of our on-premise work is On-Prem Manager, which deploys and updates the containers on each client server and snapshots their volumes. With codeshell, a client's integration is one container and one volume. On-Prem Manager can persist that volume's content to its cloud, where it is versioned and reviewed, so the integration needs no repo of its own.

What it doesn't try to be

  • Single user. One login, an HMAC-signed session cookie, and repeated failed logins from one IP are locked out for longer each time.
  • Anyone who signs in has a root shell in the container. Put it behind HTTPS and use a strong password. The password and the session secret are removed from the environment of terminals and start.sh.
  • No IntelliSense. It's an editor with syntax highlighting, quick open, split groups, find and replace, and a terminal.

The repo is github.com/Panth977/code-shell. Images are published for linux/amd64 and linux/arm64.

Where do your per-client integrations live today, and what happens to them when the client's server goes offline?

Top comments (0)