Open-source project
samuelkarp/runj avatar
samuelkarp/runj

runj is an OCI-shaped runtime for FreeBSD jails, and it says so on the box

runj is an experimental, proof-of-concept OCI-compatible runtime for FreeBSD jails.

674 stars38 forksGoNOASSERTION

At a glance

What is it?
runj maps the OCI runtime specification onto FreeBSD jails so that container tooling can drive jail based workloads. It implements a subset of the spec, adds two FreeBSD specific networking extensions, and the README states plainly that its security has not been evaluated and that it should not run on a production system.
Who is it for?
Adopt runj if you are on FreeBSD and want to drive jails with OCI shaped tooling, or if you want to test proposals for extending the runtime specification with FreeBSD capabilities, which is the testbed role the README describes. Do not adopt it for production workloads, because the README states the implementation has not been evaluated for its security and that it is a personal project.
Can I use it commercially?
Check first. The repository uses a licence we do not classify automatically, so read its LICENSE file before any commercial use.
Is it still maintained?
Yes. The repository last received commits 29 days ago.
What is it written in?
Mainly Go, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 24, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The README leads with a warning, and that warning is the review

runj describes itself as an experimental, proof-of-concept OCI compatible runtime for FreeBSD jails. The README then puts a bold Important notice immediately after the title, and the notice is not a formality. It states that runj is a proof-of-concept and that the implementation has not been evaluated for its security, tells the reader not to use it on a production system, and says not to run workloads inside it that rely on a secure configuration. It also notes that this is a personal project and not backed by the author's employer.

That framing should shape the evaluation. A runtime that starts processes in jails is exactly the kind of code where a gap in isolation becomes a security problem, and the author is telling you the audit has not happened. What you are looking at is a testbed, and the README says so in the second paragraph rather than in a footnote.

The project also says it is in early development and functional but with very limited features, and it points to docs/spec-coverage.md for a checklist of which parts of the OCI runtime specification are implemented. That file is the document to read before anything else, because the spec is broad and the coverage is partial.

Five OCI commands, a narrow config surface, two hooks

The implemented part of the specification is listed in the README and it is short. Under Commands, runj supports Create, Delete, Start, State and Kill. Under Config it supports root path, process args, process environment, process terminal, hostname and mounts, and under Hooks it supports CreateRuntime and Poststop.

That list is the honest scope statement. There is no exec, no pause or resume, no checkpoint, and no signal handling beyond the basic kill command. Anything a container orchestrator expects from a full runtime, such as exec into a running container, is outside the implemented set.

The jail specific extensions are separate and marked experimental. Under Config, runj adds IPv4 mode and IPv4 addresses. The README explains the intent: the OCI runtime spec does not currently include support for FreeBSD, so runj adds experimental support for some FreeBSD capabilities, in the spirit of rough consensus and working code, and serves as a testbed for future proposals to extend the specification. Those extensions are documented in docs/oci.md, which is where a specification proposal would start.

The repository layout matches this structure. Directories named oci/, runtimespec/, jail/, state/, hook/, containerd/, demo/, internal/ and cmd/ line up with the pieces described above, with runtimespec/ holding the spec types and the rest holding the jail facing implementation.

A jail is described by a bundle, a config.json and a root filesystem

Running a jail with runj means preparing an OCI bundle, which the README defines as a root filesystem plus a JSON formatted configuration file called config.json. The config.json is the standard OCI shape, and the README's instruction for getting started is to use runj demo spec to generate an example config file, then edit the root path and process args to the values you want.

The FreeBSD extensions can be written straight into config.json or kept separately in a file called runj.ext.json in the bundle directory, and the second option is what the containerd flow uses through the --runtime-config-path flag.

The root filesystem can be a regular FreeBSD userland or a reduced set of FreeBSD compatible programs. For experimentation, the README points at statically linked programs from /recovery that you can copy into a bundle, and for a full userland it gives the FreeBSD release archive location, with $ARCH and $VERSION replaced by your architecture and desired version.

Two convenience commands exist to shortcut all of this. runj demo download retrieves a working root filesystem from the FreeBSD website, and runj demo oci-image converts a FreeBSD root filesystem into an OCI image. The demo command prints what it found and what it is doing, so a first run looks like this:

bash
$ runj demo download --output rootfs.txz
Found arch:  amd64
Found version:  12.1-RELEASE
Downloading image for amd64 12.1-RELEASE into rootfs.txz

The conversion step then writes an image tarball with the layer digests computed along the way:

bash
$ runj demo oci-image --input rootfs.txz
Creating OCI image in file image.tar
extracting...
compressing...
computing layer digest...

Those digests are printed as sha256 values and are what the import step in the next section verifies against.

containerd 1.5 or later, and the ctr commands that drive it

The repository contains an experimental shim for containerd, published as containerd-shim-runj-v1, and the README says it can be used from the ctr command line tool by specifying the runtime name. The runtime identifier is not the usual one:

bash
$ cat <<EOF >runj.ext.json
{"network":{"ipv4":{"mode":"inherit"}}}
EOF

The same shim can drive a container through ctr with the --runtime flag set to that identifier, and the --rm flag to remove the container after it exits. The README also notes that containerd 1.5 or later is required, because earlier versions do not have all the necessary patches for FreeBSD support, and that additional functionality may be available in a development build of containerd.

For the extension experiment, the runj.ext.json file above is passed with --runtime-config-path. The mode inherit gives the container access to the host's IPv4 networking stack, which the README compares to the --net-host networking mode on Linux. The README also warns that containerd and runj will not automatically create an /etc/resolv.conf inside your container, so if the image does not include one you may need to add one yourself for name resolution to work. The minimal file it suggests is:

bash
nameserver 8.8.8.8

That is a small detail with a large effect. A container that starts but cannot resolve a hostname is a confusing failure, and the fix is a file the tooling does not create for you.

Jails are driven through syscalls, with four command line tools pulled in

The implementation section explains how runj talks to FreeBSD. It directly invokes FreeBSD's jail related syscalls, but some command line utilities are still necessary. The README names them: mount(8) for mounting filesystems, because the Go runtime does not implement mounting directly on FreeBSD, ifconfig(8) for moving VNet interfaces into a jail, ps(1) outside the jail for inspecting jail processes, and jexec(8) for running a process inside a jail.

That list is a fair summary of where a Go port of a container runtime hits the edges of the operating system. Mounting and interface manipulation are the two that stay in userspace, which means runj inherits the behaviour and the permission requirements of those tools. The README excerpt cuts off mid sentence at jexec(8), so the full list of helpers is longer than what is shown here.

The direct jail syscalls are the interesting part. Rather than wrapping a higher level tool, runj talks to the kernel mechanism that FreeBSD already ships, which is why the result is an OCI-shaped interface over something that predates containers. The consequence for you is that the isolation model is the jail model, with its documented jail semantics, not a Linux namespace sandbox.

The Makefile builds three binaries and checks its own licence notice

The build is plain Go with a Makefile at the root, and it produces three binaries rather than one: the runtime itself, an entrypoint helper, and the containerd shim. The build targets are named after the binaries they produce:

bash
	go build -o bin/runj ./cmd/runj
bash
	install -o 0 -g 0 bin/runj bin/runj-entrypoint bin/containerd-shim-runj-v1 /usr/local/bin

The install recipe copies all three into /usr/local/bin owned by root, which tells you the intended deployment is a system level install on a FreeBSD host rather than a per user binary.

The lint recipe is worth noticing because it pins the target OS:

bash
	GOOS=freebsd golangci-lint run

The repository also carries a NOTICE target that runs go-licenses report over the module and renders it through hack/notice.tpl, and a verify-notice target that regenerates the file and diffs it against the committed copy. That is a licence compliance check in CI form, and it is the mechanism behind the NOTICE file in the repository root. The licence itself is a custom one that GitHub cannot classify, so the details live in the LICENSE file and the dependency inventory lives in NOTICE.

The module is go.sbk.wtf/runj and requires go 1.25.0. Its dependencies lean on containerd's libraries directly, including containerd/api, containerd/v2, go-runc, fifo, ttrpc, typeurl and plugin, along with the OCI runtime-spec and image-spec modules, cobra for the command line, logrus for logging and moby/sys/mount for mount handling.

Two releases in two years, and a project that still calls itself early

The release history has two entries. v0.1.0, described as the initial release, was published on 2024-01-05, and v0.2.0 was published on 2025-11-06. The last push was on 2026-09-25, and the repository is not archived.

So the project is alive without being fast moving. Almost a year has passed since the v0.2.0 tag, and the README still describes runj as being in early development and functional but with very limited features. The version scheme itself signals the stage, since a project expecting compatibility promises does not stay at 0.2 for two years.

That combination, a recent push and slow releases, is consistent with a project that advances when its author has time rather than on a schedule. For a proof of concept that is a reasonable shape. For anything you intend to depend on in a pipeline, the practical question is whether the spec coverage in docs/spec-coverage.md will grow, and how long you are willing to wait for the parts that matter to your workload.

The licence question is separate and should be checked directly. The repository carries a LICENSE file with terms GitHub's classifier could not categorise, plus a NOTICE file generated from the dependency tree, and a CONTRIBUTING.md at the root. Read the LICENSE file before you ship anything built on it, since the licence identifier in the repository listing is deliberately left unclassified rather than set to a standard permissive identifier.

Editorial conclusion

Adopt runj if you are on FreeBSD and want to drive jails with OCI shaped tooling, or if you want to test proposals for extending the runtime specification with FreeBSD capabilities, which is the testbed role the README describes. Do not adopt it for production workloads, because the README states the implementation has not been evaluated for its security and that it is a personal project. Verify first which spec parts are covered in docs/spec-coverage.md, whether your FreeBSD version matches the base image you intend to use, and that the containerd version in use is 1.5 or later, since earlier releases lack the required FreeBSD patches.

Frequently asked questions

Is runj safe to use in production?

The README states that runj is a proof-of-concept whose implementation has not been evaluated for its security, says not to use it on a production system, and warns against running workloads that rely on a secure configuration. It is also described as a personal project.

Which parts of the OCI runtime specification does runj implement?

The README lists Create, Delete, Start, State and Kill under Commands, and root path, process args, process environment, process terminal, hostname, mounts, and the CreateRuntime and Poststop hooks under Config. The full checklist is in docs/spec-coverage.md.

How do I create and run a jail with runj?

Prepare an OCI bundle with a root filesystem and a config.json, generate a sample with runj demo spec, then use runj create with an identifier and the bundle path, runj start to launch the process, runj state to inspect it, runj kill to signal it and runj delete to remove it.

What containerd version is needed to use the runj shim?

containerd 1.5 or later is required, because earlier versions do not have all the necessary patches for FreeBSD support. The shim is named containerd-shim-runj-v1 and is selected in ctr with the --runtime flag set to wtf.sbk.runj.v1.

Why does my runj container have no DNS resolution?

The README notes that containerd and runj do not automatically create an /etc/resolv.conf file inside the container. If your image does not include one, you may need to add it yourself, for example with a nameserver 8.8.8.8 line.

Official sources

  1. Issues
  2. README
  3. Releases
  4. samuelkarp/runj on GitHub
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/samuelkarp-runj.svg)](https://hysenlabs.com/projects/samuelkarp-runj)

Was this useful?