Skip to content

Setting up JailBee in your own project

jailbee is project-agnostic — every repo provides its own .jailbee/config.yaml. The built-in defaults are stack-neutral: the golden image installs only JailBee's own plumbing, and language toolchains (JDK, Node, Python, Docker) are bundled but opt-in per repo via golden.stacks.

1. Drop a config into your repo

cd /path/to/your/repo
uv tool run jailbee config init   # writes .jailbee/config.yaml

2. Set container_prefix if needed

Defaults to repo_root.name. Required override if your directory has uppercase, dots, or underscores (Incus accepts only [a-z0-9-]):

container_prefix: sampleapp

3. Configure host mounts

Bind your dev credentials and tools into the container. User-declared host_mounts are read-write by default — set readonly: true explicitly for anything sensitive:

host_mounts:
  - { host: ~/.gnupg,    container: /home/dev/.gnupg,    readonly: true }
  - { host: ~/.gitconfig, container: /home/dev/.gitconfig, readonly: true }
  - { host: ~/.ssh,      container: /home/dev/.ssh,      readonly: true }

Sharing files with .local

If a .local/ directory exists at the repo root, jailbee new automatically bind-mounts it read-write into each new container at ~/<container_prefix>/.local (e.g. ~/SampleApp/.local). It's a quick host⇄container scratch channel — drop a script there from inside the container and run it on the host, or vice-versa. The directory is added to the container clone's .git/info/exclude, so it never shows up as untracked in jailbee ls or jailbee git diff.

Presence-triggered: nothing happens unless .local/ already exists (it is never auto-created). Disable with share_local: false in .jailbee/config.yaml. --mount containers already expose it via the full-repo bind, so the auto-mount is skipped there. Existing containers pick it up on the next jailbee new.

Sharing host sockets

A container shares the host's kernel, so a unix socket on the host can be used from inside it — the container connects to the host process on the other end instead of running its own copy. JailBee already does this for you: the Wayland, PulseAudio, D-Bus and GnuPG sockets under /run/user/<uid> are attached to each container at the same path, which is what makes jailbee chrome render on your desktop, and what makes git commit -S and ssh work inside a container while the key stays on the host. With gpg.enabled, SSH_AUTH_SOCK points at the host gpg-agent's SSH socket, so a smartcard-backed key authenticates from inside the container without any private key being copied into it — the agent signs, the container only asks.

The same trick is yours to use for anything else the host has a socket for. An adb example follows; a host database socket, a media daemon or a license server work the same way. Note that this is a container-only capability: a VM-based sandbox has no host kernel to share, so it cannot mount a host socket at all.

Talking to Android devices over adb

adb inside a container can drive a device or emulator attached to the host — no USB passthrough, no second adb server. Forward the host's adb server port into the container:

host_ports:
  - { name: adb, port: 5037 }

That's the whole recipe. The host's adb server already listens on 127.0.0.1:5037 by default, so once the forward is attached, plain adb devices inside the container lists whatever the host has plugged in — no ADB_SERVER_SOCKET, no mount. For existing containers, run jailbee apply to attach the forward (proxy devices hotplug, so no restart is needed). See host_ports for the full field grammar.

If something inside the container is already listening on 5037 — most likely its own adb server, started by an adb invocation before the forward was attached — the forward cannot be attached, because instance-side proxy listeners and pre-existing listeners on the same port can't coexist. Incus's own message for that case names neither the port nor the cause (Failed to receive fd from listener process: Failed to receive file descriptor via abstract unix socket), which is why jailbee port to-container translates it into something actionable: stop the container's own adb server (or forward to a different container port) and retry.

Fallback: bind-mount the socket. host_ports only forwards TCP/UDP ports; the schema deliberately does not expose unix-socket endpoints. For a service that is only ever reachable over a unix socket, bind-mount it instead and point the client at it explicitly:

host_mounts:
  - { host: ~/.android/adb.sock, container: /home/dev/.adb.sock, readonly: false }

container:
  env:
    ADB_SERVER_SOCKET: "localfilesystem:/home/dev/.adb.sock"

The mount is read-write on purpose: a socket the container can only read is a socket it cannot talk on. On the host, the adb server has to be listening on that same socket rather than on its default port — start it with adb -L localfilesystem:$HOME/.android/adb.sock start-server (or export the same ADB_SERVER_SOCKET on the host). After that, adb devices inside the container lists what the host has plugged in, and every container sharing the socket sees the same devices.

To run the emulator inside the container instead, pass the KVM node through with host_devices: [{ path: /dev/kvm }] — see host_devices. That gives the container its own emulator and its own adb server, isolated from the host's.

4. Optional — stack runtimes and extra apt packages

The golden image is stack-neutral by default (locale, prompt, GUI libraries, gh, tmux, build-essential). Language runtimes and cloud helpers — Node, Java, Docker, Python, the ECR helper — are bundled but opt-in. The recommended way to turn them on is golden.stacks, one key per runtime — it also wires up the matching shared caches and build-env values (JAVA_PACKAGE, NODE_MAJOR):

golden:
  stacks:
    node: 22          # major version, or `true` for the default
    docker: true
  extra_apt_packages:
    - mariadb-client
    - postgresql-client

See Stacks in config.md for the full grammar, including the Java openjdk-N / corretto-N forms and the java + dockerregistry-mirror-ca auto-add. golden.enable_snippets (stage a snippet by name) plus manual shared_caches remain available directly as the low-level escape hatch for anything stacks doesn't cover.

Package names for extra_apt_packages are validated against the Debian grammar ([a-z0-9][a-z0-9+\-.]*) — anything else is rejected at config-load time. Run jailbee base build after editing to rebuild the image.

4b. Optional — override the provisioning script entirely

If extra_apt_packages isn't enough, replace the whole install.sh:

golden:
  provision_script: ./.jailbee/install.sh
  provision_env:
    REGION: eu-north-1   # whatever your script reads

4c. Optional — enable a coding agent

agents: is valid at both ~/.config/jailbee/global.yaml (your personal default across every repo) and this repo's .jailbee/config.yaml (a team default for everyone working on it) — a repo entry merges over a global one for the same agent, field by field, same as the rest of JailBee's config layering. Enabling a shipped preset is usually two lines:

agents:
  claude:
    enabled: true
    autostart: true

Six presets ship (claude, codex, gemini, aider, opencode, grok); only claude is exercised in production, the rest are untested starting points. See Generic agent support for the full mechanism, the preset table, and how to write your own agent entry.

5. Define autostart steps

Each step is a shell command run as the dev user inside the container. on_create fires on jailbee new; on_start fires on jailbee start.

autostart:
  step_timeout: 600
  env:
    NODE_ENV: development
  on_create:
    - name: setup
      run: "make setup"
    - name: server
      run: "make run"
      working_dir: backend
      background: true
      mounts: [aws]      # attach optional_mounts.aws for this step only

Auto-launching an IDE or a browser is configured outside the autostart block: set jetbrains.ide + jetbrains.autostart, and browsers.chrome.autostart / browsers.firefox.autostart, in your config (a plain apps: entry has its own apps.<name>.autostart). See config.md for the full step-field reference and those keys.

6. Build the image and create your first container

jailbee init                          # create profiles + ACL
jailbee base build                    # 10–15 min, one-time
jailbee new feat/x                    # new branch off default (e.g. dev)
jailbee new feat/x feat/wip-bar       # new branch off feat/wip-bar
jailbee new feat/jokufeat             # check out existing branch for review
jailbee new --current                 # use host repo's currently checked-out branch