code-server on FreeBSD

🇩🇪 Diesen Artikel gibt es auch auf Deutsch.

VS Code in the browser, native on FreeBSD, and why it took years to get there

After BunkerWeb, here is the next porting chronicle: code-server: Visual Studio Code as a server-side application in the browser. Since June 21, 2026, www/code-server has been part of the FreeBSD Ports Collection. And the road there was anything but an ordinary Node.js port.


1. Why code-server on FreeBSD?

code-server takes the open-source core of VS Code and turns it into a web service: the UI runs in the browser, while the actual server, files, terminals, Git, search, extensions, runs under Node.js on the target system. For FreeBSD that is a perfect fit: development directly on servers and in jails, without a local Electron client, from any device.

The real game-changer becomes obvious when you compare it with the seemingly natural alternative: VS Code on your Mac or Windows machine, plus the Remote-SSH extension pointed at the FreeBSD box. That simply does not work. Remote-SSH installs a VS Code Server on the target system, and Microsoft only publishes that server for Linux (plus macOS and Windows). FreeBSD binaries just do not exist.

For years the community has resorted to workarounds via the Linuxulator: linux_enable="YES", install a Linux base, fake a Linux shell via RemoteCommand /compat/linux/usr/bin/bash, and tell VS Code the platform is „Linux“. Reading the relevant forum threads shows how fragile that is: PATH contortions, hanging connections, the server’s glibc requirements, and every VS Code update can break the whole construction again.

code-server simply turns the problem around: instead of tricking a Linux binary into accepting a FreeBSD system, the editor server runs natively on FreeBSD, as a clean package with an rc.d script. The browser is the client, whether on macOS, Windows or Linux. No Linuxulator, no faked platform, nothing that breaks on the next update.

Sounds like a simple Node.js port otherwise? It is not. A single application combines browser sandboxing, server-side Node.js, TypeScript/esbuild/Gulp build chains, native C++ add-ons, VS Code product files, and marketplace infrastructure. That is why a „pure JavaScript port“ suddenly needs compilers, Python, node-gyp and extensive operating system patches.


2. The backstory, and a chance discovery

I made the decision to port code-server independently of anything that came before, I simply wanted the tool natively on FreeBSD. Only during my research did I stumble, more or less by accident, upon earlier attempts: PR 256144 from 2021, for example, and further tries later on, some of which effectively moved the entire build out of the port.

The find was interesting nonetheless: it showed that the core problem had been known for years, an npm install during the build, i.e. network access that a regular Poudriere build simply does not allow. Every previous attempt had either failed at exactly that point or built its way around it.

So I set clear goals for my approach from the start: the source code keeps being built inside the port, FreeBSD binary modules are created inside the port, Poudriere needs no network whatsoever, and updates follow a documented, repeatable release process.


3. Why a plain npm ci is not enough

For a simple Node.js project the workflow would be: npm ci, npm run build, done. For a FreeBSD port this fails twice over.

First: no network during the build. Poudriere expects all source files to be known in the fetch phase. An npm ci that downloads thousands of packages during do-build violates that principle, and it does not stop at npm itself: prebuild-install, node-pre-gyp, Git downloads from build scripts, prebuilt add-ons fetched on the fly. The code-server source tree contained build scripts with npm install, curl, wget and Linux container builds. The postinstall script could even run npm install again on its own if it saw the need.

Second: there is not just one node_modules tree. code-server has its own dependencies, but the embedded VS Code source tree contains a whole landscape of separate npm projects alongside it:

lib/vscode/
lib/vscode/build/
lib/vscode/extensions/   # dozens of built-in extensions, each its own npm project
lib/vscode/remote/
lib/vscode/remote/web/

A single archive at the project root is not enough. The dependencies have to live in exactly the subprojects where the VS Code build chain expects them.


4. The solution: a reproducible offline build

The architecture follows the principle already used by editors/vscode: for every version, prepared archives are provided as distfiles.

code-server-node-modules-${DISTVERSION}.tar.gz   # npm tree of code-server itself
vscode-node-modules-${VSCODE_VERSION}.tar.gz     # npm trees of all VS Code subprojects
vscode-reh-web-linux-x64-${DISTVERSION}.tar.gz   # Remote Extension Host

They are created via a bootstrap target inside the port (make-node-modules-archive) that runs a controlled npm ci --ignore-scripts and packages the result. The archives are published as release assets in the joneum/FreeBSD-CodeServer repository; they are built on a separate build VM so that bootstrap downloads and the actual Poudriere test stay cleanly separated.

Importantly, the port does not install a fully prebuilt package. Compilation still happens inside the port, code-server, VS Code, the native FreeBSD modules. Only what cannot reasonably be expressed with the FreeBSD fetch infrastructure is prepared up front. That keeps the port much closer to the classic ports model than a pure binary repackaging port.

One detail deserves explanation: the remote host artifact keeps its linux-x64 name even though the result runs on FreeBSD. The VS Code build logic simply knows no FreeBSD target for this build, so linux-x64 serves as a structural template whose content is then made FreeBSD-ready with FreeBSD binary modules and patches. That is also why the port is limited to amd64 for now.


5. The FreeBSD patches: a selection

Hard platform checks

The most common failure class of the entire porting effort looks like this:

switch (process.platform) {
  case "win32":
  case "darwin":
  case "linux":
    break;
  default:
    throw new Error("Unsupported platform");
}

Code that would be perfectly Unix-compatible fails, merely because freebsd is missing from a list. Among the casualties: @vscode/deviceid and the built-in microsoft-authentication extension, whose esbuild script terminated the Remote Extension Host build with Error: Unsupported platform: freebsd.

The lesson about multiple copies

The most instructive bug of the port: the first deviceid patch was applied to a node_modules tree that a later build step regenerated, by staging time, the patch had vanished again. The final solution patches the generated copy inside the Remote Extension Host directly, after npm run build:vscode. It is not enough to know which file needs patching, what matters is in which build phase the copy that actually gets installed comes into existence.

node-pty: no terminal, no editor

node-pty connects the integrated terminal to the operating system’s pseudo-terminals, native C++ code with no FreeBSD prebuilds. So: adapt the source (including the call to pty_close_inherited_fds()), rebuild with node-gyp against the FreeBSD Node headers, place the result as prebuilds/freebsd-x64/pty.node, and copy the same binary into the Remote Extension Host. Same game for @vscode/spdlog (logging) and @vscode/native-watchdog, which only built once devel/libuv was added as a dependency.

Small stuff that stops the build

  • Git assumptions: upstream determined the build ID via git rev-parse HEAD: but a port works with extracted distfiles, not a Git checkout. The patch accepts a preset BUILD_SOURCEVERSION.
  • Wrong target directory: build scripts kept changing into the wrong directory despite the mapped target, solved via a separate GULP_VSCODE_TARGET variable.
  • Configuration paths: userDataPath.ts mostly knew Linux and macOS under Unix; FreeBSD is now mapped to the proper XDG path model.
  • tsgo: newer typecheck steps use binaries that do not exist for FreeBSD, those specific targets are skipped, while the actual compilation runs in full.
  • Linux binaries in the tree: embedded ELF files like rg and tgrep are replaced with native FreeBSD binaries from textproc/ripgrep and textproc/tgrep: the paths keep saying linux-x64, the content is FreeBSD.

6. Operations: a clean FreeBSD service

Installation works as usual:

pkg install code-server

sysrc code_server_enable=YES
service code-server start

The rc.d script is more than convenience, it answers the operational questions: which user the service runs as, where config.yaml, user data and extensions live, which home and XDG directories Node.js sees. The port installs a wrapper at /usr/local/bin/code-server and the application tree separately at /usr/local/share/code-server. For maintainability the port is split into several Makefile fragments (Makefile, Makefile.vscode, Makefile.build, Makefile.install), plus a very large pkg-plist, because many thousands of files get installed.


7. Extensions: why not everything works

The extension gallery is Open VSX, the Eclipse Foundation’s open registry, unofficial VS Code products may not simply use the Microsoft Marketplace. To be honest about it: not every extension from your usual VS Code is available. Some are missing at the registry level, because the vendor only publishes to the Microsoft Marketplace or Open VSX only carries an older version. But even „installs fine“ does not mean „works completely“. The main reasons:

  • Workspace extensions run on the server. They see process.platform === "freebsd": not the browser’s operating system. Bundled Windows, macOS or Linux binaries do not help there.
  • Bundled binaries: many extensions ship language servers, debug adapters or native add-ons for linux-x64, darwin-arm64, win32-x64. If freebsd-x64 is missing, activation fails, and a Linux binary does not automatically run under the Linuxulator either.
  • Downloads on first start: some extensions assemble their download URL from process.platform. For freebsd there simply is no URL.
  • Electron and Microsoft specifics: extensions expecting the desktop Electron process, proprietary Microsoft services or the official VS Code product identity work partially or not at all. Remote-WSL, Dev Containers and Microsoft Remote-SSH do not transfer conceptually, though code-server solves the remote problem differently anyway: the editor server already runs directly on FreeBSD.
  • Version coupling: what matters for engines.vscode is the embedded VS Code version, code-server 4.123.0 shipped VS Code 1.122.1, for example, not 1.123.

One edge case deserves its own mention: GitHub Copilot. The VS Code build expects platform-specific Copilot packages and would otherwise try to fetch them via npm, impossible in Poudriere. The port therefore creates local package structures so the open part builds. Copilot still is not usable on FreeBSD: the component is closed, and where neither source code nor FreeBSD binaries exist, portability ends.


8. Maintenance: an update is more than make makesum

code-server and the embedded VS Code have separate version numbers that evolve independently. Every update therefore means: which VS Code version does code-server expect? Have the npm subprojects changed? New native modules? Do the patches still apply? New platform checks? New binaries in the remote web build? Add fresh node_modules archives, Poudriere runs and extension tests. The port is, honestly, maintenance-intensive, which is exactly why it belongs with someone who maintains it long-term.


Conclusion

The biggest obstacle was not FreeBSD. It was a modern JavaScript build chain that assumes it may download packages and binaries from the internet at any point during the build. FreeBSD is rarely fundamentally incompatible, usually what is missing are platform paths, binary artifacts, or a reproducible build process. Open source can be adapted and recompiled; only closed components are a hard stop.

Since June 21, 2026, www/code-server (initially 4.123.0, amd64) has been part of the Ports Collection, sponsored by Netzkommune GmbH. Along the way, the port also documents how a very large, modern TypeScript project can be integrated into the reproducible FreeBSD ports model.

If you would like to support my work on this and more than 40 other ports, you will find the option to do so in the sidebar.

Are you running code-server on FreeBSD, or planning to, in a jail? Experiences and extension reports are very welcome in the comments!

Schreibe einen Kommentar