Skip to content

feat: containerd/nerdctl engine backend#445

Draft
AaronFeledy wants to merge 69 commits intomainfrom
feat/containerd-engine
Draft

feat: containerd/nerdctl engine backend#445
AaronFeledy wants to merge 69 commits intomainfrom
feat/containerd-engine

Conversation

@AaronFeledy
Copy link
Member

@AaronFeledy AaronFeledy commented Mar 14, 2026

Summary

Adds containerd + nerdctl + BuildKit as an alternative container engine backend. Lando can run independently of Docker with engine: "containerd" in config.

What

  • Backend abstractionDaemonBackend, ContainerBackend, ComposeBackend interfaces
  • Docker backend — existing code wrapped as implementations, zero behavior changes
  • Containerd backendContainerdDaemon (containerd + buildkitd + finch-daemon lifecycle), ContainerdContainer (nerdctl ops), NerdctlCompose (compose adapter)
  • finch-daemon — Docker API compatibility socket for Traefik proxy
  • Platform support — Linux/WSL2 native, macOS via Lima VM
  • Setup UXlando setup prompts for engine selection, downloads binaries from GitHub
  • Doctor checks — binary/daemon/connectivity health checks
  • Error messages — 9 user-friendly containerd-specific messages
  • Perf tooling — timer utility, benchmark script, debug-mode perf logging

Config

engine: containerd  # or "docker" (default) or "auto"

Stats

72 files changed, ~10,000 lines added. 467 tests passing. No existing behavior changed — Docker path works exactly as before.

Not yet done

  • Leia integration tests do not run against containerd in CI (default is auto → Docker)
  • Needs a dedicated CI job with engine: containerd to exercise the backend end-to-end

Note

High Risk
Introduces a new container engine backend (containerd/nerdctl) and changes engine bootstrap/setup paths, which can impact core lifecycle, orchestration, and proxy behavior across platforms. Risk is mitigated by preserving the existing Docker codepath but the surface area is large and includes new daemon/process management.

Overview
Adds an experimental containerd engine option (and default engine: auto) that lets Lando run via containerd + buildkitd + nerdctl instead of Docker, including engine selection during lando setup and containerd-specific compatibility/health checks.

Replaces utils/setup-engine.js bootstrapping with a new BackendManager that instantiates either the Docker-backed engine or a new containerd-backed engine; the containerd path manages its own daemons, uses nerdctl compose for orchestration, and introduces a Docker-API compatibility socket (finch-daemon) to keep the Traefik proxy working.

Extends CI to run core Leia tests against both docker and containerd, adds engine documentation and developer benchmarking tooling (scripts/benchmark-engines.sh).

Written by Cursor Bugbot for commit 9f53c2d. This will update automatically on new commits. Configure here.

Define explicit base classes (DaemonBackend, ContainerBackend,
ComposeBackend, EngineBackend) that any container engine backend
must implement. Extracted from existing Docker/Dockerode code.

Includes all 14 Engine facade methods, router dispatch documentation,
abstract class guards, and comprehensive JSDoc contracts.

Part of the containerd/nerdctl engine initiative.
Create DockerDaemon, DockerContainer, DockerCompose classes that
implement the backend interfaces by delegating to existing
LandoDaemon, Landerode, and compose.js code.

Uses getter/setter proxying for live property access.
No existing files modified - full backward compatibility.

Part of the containerd/nerdctl engine initiative.
ContainerdDaemon manages Lando's own isolated containerd + buildkitd
instances. Handles lifecycle (up/down/isUp), PID management, socket
health checks, stderr logging, and elevated (sudo) starts with PID
discovery.

Platform support: Linux/WSL native, macOS/Windows stubbed with
helpful errors pending Lima VM integration.

Part of the containerd/nerdctl engine initiative.
ContainerdContainer implements ContainerBackend by shelling out to
nerdctl for all container operations. Includes JSONL parsing,
label normalization (handles commas in values), proxy objects for
getContainer/getNetwork, and the full Lando container filtering
pipeline from Landerode.list().

Part of the containerd/nerdctl engine initiative.
NerdctlCompose extends ComposeBackend by delegating to the existing
compose.js command builder and prepending nerdctl --address <socket>
compose to every command array. Zero duplicated logic — just a thin
transform layer.

Part of the containerd/nerdctl engine initiative.
BackendManager factory creates the right Engine based on config.engine
setting (auto | docker | containerd). Auto-detection prefers containerd
if all binaries exist, falls back to Docker.

New config defaults: engine, containerdBin, nerdctlBin, buildkitdBin,
containerdSocket. All non-breaking (engine defaults to auto, overrides
default to null).

setup-engine-containerd.js provides standalone containerd wiring.
Existing setup-engine.js and lando.js untouched.

Part of the containerd/nerdctl engine initiative.
Utility modules for locating and downloading containerd stack binaries:
- get-containerd-x.js, get-nerdctl-x.js, get-buildkit-x.js (binary resolution)
- get-containerd-download-url.js (GitHub release URL construction)
- setup-containerd-binaries.js (download + install missing binaries)

Follows existing get-docker-x.js patterns. Supports linux/darwin,
amd64/arm64.

Part of the containerd/nerdctl engine initiative.
75 unit tests covering BackendManager, NerdctlCompose, ContainerdContainer
(including parseLabels comma-in-value fix), and download URL generation.
All passing.

Documentation for the new engine config option at docs/config/engine.md.

Part of the containerd/nerdctl engine initiative.
Replace setup-engine.js call with BackendManager.createEngine() in
bootstrapEngine(). Engine selection now driven by config.engine
setting (auto | docker | containerd).

Old setup-engine.js call kept as commented reference.
BackendManager exposed as lando.backendManager for plugins.

Part of the containerd/nerdctl engine initiative.
Setup hook downloads containerd, buildkitd, and nerdctl from GitHub
releases during 'lando setup'. Skips when engine=docker.
Check hook warns when engine=containerd but binaries are missing.

Part of the containerd/nerdctl engine initiative.
LimaManager class handles Lima VM lifecycle (create/start/stop/exec)
for running containerd on macOS. ContainerdDaemon now creates and
manages a Lima VM on darwin instead of throwing 'not implemented'.

Exposes containerd socket at ~/.lima/lando/sock/containerd.sock.

Part of the containerd/nerdctl engine initiative.
Engine.getCompatibility() now handles both Docker and containerd
version formats. Adds supportedContainerdVersions config,
engineBackend property, and containerd-aware dockerInstalled/
composeInstalled logic.

Part of the containerd/nerdctl engine initiative.
Wire lando-setup-containerd-engine into pre-setup event and
lando-setup-containerd-engine-check into pre-engine-autostart.

Part of the containerd/nerdctl engine initiative.
31 test cases covering BackendManager, ContainerdDaemon lifecycle,
ContainerdContainer operations, NerdctlCompose command generation,
and full engine lifecycle. Tests requiring real containerd auto-skip.

Part of the containerd/nerdctl engine initiative.
app-check-containerd-compat.js validates containerd/nerdctl/buildkit
versions and reports warnings. lando-get-containerd-compat.js runs
Engine.getCompatibility() for containerd backends.

Part of the containerd/nerdctl engine initiative.
WslHelper handles WSL-specific concerns: custom containerd config to
avoid Docker Desktop conflicts, socket permission management, and
CRI plugin disabling. ContainerdDaemon auto-detects WSL and writes
config before starting containerd.

Part of the containerd/nerdctl engine initiative.
Register lando-get-containerd-compat in index.js and
app-check-containerd-compat in app.js. Confirm engine.docker
proxy works with ContainerdContainer (same interface, no gap).

Part of the containerd/nerdctl engine initiative.
Populates lando.versions with containerd/nerdctl/buildkit versions
when running on the containerd backend.

Part of the containerd/nerdctl engine initiative.
Standalone bash script that exercises the full containerd engine path:
start containerd, start buildkitd, compose up nginx, verify, cleanup.

Part of the containerd/nerdctl engine initiative.
- Add explicit this.containerd property on ContainerdDaemon
- Add engine, binary paths, and supportedContainerdVersions to
  index.js defaults for discoverability
- Remove hardcoded fallback from Engine constructor

Part of the containerd/nerdctl engine initiative.
@netlify
Copy link

netlify bot commented Mar 14, 2026

Deploy Preview for lando-core ready!

Name Link
🔨 Latest commit a1ce102
🔍 Latest deploy log https://app.netlify.com/projects/lando-core/deploys/69b73e53fdd229000841b608
😎 Deploy Preview https://deploy-preview-445--lando-core.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
Lighthouse
Lighthouse
1 paths audited
Performance: 87 (no change from production)
Accessibility: 89 (no change from production)
Best Practices: 92 (no change from production)
SEO: 90 (no change from production)
PWA: -
View the detailed breakdown and full score reports

To edit notification comments on pull requests, go to your Netlify project configuration.

@AaronFeledy AaronFeledy changed the title feat: containerd/nerdctl engine backend Experiment: containerd/nerdctl engine backend Mar 14, 2026
@AaronFeledy
Copy link
Member Author

@cursor review

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 4 potential issues.

Autofix Details

Bugbot Autofix prepared fixes for all 4 issues found in the latest run.

  • ✅ Fixed: Compose wrapper missing default for opts parameter
    • Added || {} default to datum.opts in both Docker and containerd compose wrappers to match original setup-engine.js behavior.
  • ✅ Fixed: Warning function ignores version info argument
    • Updated update-nerdctl-warning to accept and display version, update, and link parameters in the warning message.
  • ✅ Fixed: Unbounded recursion in container list retry logic
    • Added _retryCount parameter with a limit of 10 retries to prevent stack overflow in containerd-container list method.
  • ✅ Fixed: Containerd getVersions returns false causing semver.clean crash
    • Added filtering to remove false and 'skip' values from containerd versions before processing in getCompatibility.

Create PR

Or push these changes by commenting:

@cursor push abd2ef0b38
Preview (abd2ef0b38)
diff --git a/lib/backend-manager.js b/lib/backend-manager.js
--- a/lib/backend-manager.js
+++ b/lib/backend-manager.js
@@ -54,7 +54,7 @@
    * Returns a fully wired `Engine` instance ready for use by `lando.engine`.
    *
    * @param {string} [id='lando'] - The Lando instance identifier.
-   * @returns {Engine} A configured Engine instance.
+   * @return {Engine} A configured Engine instance.
    */
   createEngine(id = 'lando') {
     const engineType = this.config.engine || 'auto';
@@ -80,7 +80,7 @@
    * - Returns `new Engine(daemon, docker, compose, config)`
    *
    * @param {string} id - The Lando instance identifier.
-   * @returns {Engine} A Docker-backed Engine instance.
+   * @return {Engine} A Docker-backed Engine instance.
    * @private
    */
   _createDockerEngine(id) {
@@ -104,7 +104,7 @@
     );
 
     const compose = (cmd, datum) => {
-      const run = dockerCompose[cmd](datum.compose, datum.project, datum.opts);
+      const run = dockerCompose[cmd](datum.compose, datum.project, datum.opts || {});
       return this.shell.sh([orchestratorBin].concat(run.cmd), run.opts);
     };
 
@@ -124,7 +124,7 @@
    * `{cmd, opts}` shell descriptor, then executes via `shell.sh([nerdctlBin, ...cmd], opts)`.
    *
    * @param {string} id - The Lando instance identifier.
-   * @returns {Engine} A containerd-backed Engine instance.
+   * @return {Engine} A containerd-backed Engine instance.
    * @private
    */
   _createContainerdEngine(id) {
@@ -171,7 +171,7 @@
     // as the Docker path. Gets {cmd, opts} from NerdctlCompose, then executes
     // via shell.sh([nerdctlBin, ...cmd], opts).
     const compose = (cmd, datum) => {
-      const run = nerdctlCompose[cmd](datum.compose, datum.project, datum.opts);
+      const run = nerdctlCompose[cmd](datum.compose, datum.project, datum.opts || {});
       return this.shell.sh([nerdctlBin].concat(run.cmd), run.opts);
     };
 
@@ -193,7 +193,7 @@
    * Logs which engine was selected.
    *
    * @param {string} id - The Lando instance identifier.
-   * @returns {Engine} An Engine instance using the auto-detected backend.
+   * @return {Engine} An Engine instance using the auto-detected backend.
    * @private
    */
   _createAutoEngine(id) {

diff --git a/lib/backends/containerd/containerd-container.js b/lib/backends/containerd/containerd-container.js
--- a/lib/backends/containerd/containerd-container.js
+++ b/lib/backends/containerd/containerd-container.js
@@ -15,7 +15,7 @@
  * Helper to determine if any file exists in an array of files.
  *
  * @param {Array<string>} files - Array of file paths to check.
- * @returns {boolean}
+ * @return {boolean}
  * @private
  */
 const srcExists = (files = []) => _.reduce(files, (exists, file) => fs.existsSync(file) || exists, false);
@@ -33,7 +33,7 @@
  * - Labels whose values contain `,` within values that also contain `=`
  *
  * @param {string|Object} labels - Labels string from nerdctl or object from inspect.
- * @returns {Object} Docker-compatible labels object.
+ * @return {Object} Docker-compatible labels object.
  * @private
  */
 const parseLabels = labels => {
@@ -91,7 +91,7 @@
  * - `Status`   → status text
  *
  * @param {Object} nerdctlContainer - A parsed JSON line from `nerdctl ps --format json`.
- * @returns {Object} Docker API-compatible container object.
+ * @return {Object} Docker API-compatible container object.
  * @private
  */
 const normalizeContainer = nerdctlContainer => {
@@ -164,7 +164,7 @@
    * "No such container", "no such object", "not found".
    *
    * @param {Error} err - The error to inspect.
-   * @returns {boolean} `true` if the error indicates a missing resource.
+   * @return {boolean} `true` if the error indicates a missing resource.
    * @private
    */
   _isNotFoundError(err) {
@@ -184,7 +184,7 @@
    * @param {Array<string>} args - nerdctl subcommand and arguments.
    * @param {Object} [opts={}] - Additional options passed to `run-command`.
    * @param {boolean} [opts.ignoreReturnCode=false] - Whether to suppress non-zero exit errors.
-   * @returns {Promise<string>} The trimmed stdout from the command.
+   * @return {Promise<string>} The trimmed stdout from the command.
    * @throws {Error} If the command exits non-zero and `ignoreReturnCode` is false.
    * @private
    */
@@ -216,7 +216,7 @@
    *
    * @param {string} name - The name of the network to create.
    * @param {Object} [opts={}] - Additional network creation options.
-   * @returns {Promise<Object>} Network inspect data.
+   * @return {Promise<Object>} Network inspect data.
    */
   async createNet(name, opts = {}) {
     const args = ['network', 'create'];
@@ -254,7 +254,7 @@
    * Docker-compatible JSON.
    *
    * @param {string} cid - A container identifier (hash, name, or short id).
-   * @returns {Promise<Object>} Container inspect data.
+   * @return {Promise<Object>} Container inspect data.
    * @throws {Error} If the container does not exist.
    */
   async scan(cid) {
@@ -270,7 +270,7 @@
    * to prevent race conditions when containers are removed between checks.
    *
    * @param {string} cid - A container identifier.
-   * @returns {Promise<boolean>}
+   * @return {Promise<boolean>}
    */
   async isRunning(cid) {
     try {
@@ -303,9 +303,10 @@
    * @param {string}  [options.project] - Filter to a specific project name.
    * @param {Array<string>} [options.filter] - Additional `key=value` filters.
    * @param {string}  [separator='_'] - Container name separator.
-   * @returns {Promise<Array<Object>>} Array of Lando container descriptors.
+   * @param {number}  [_retryCount=0] - Internal retry counter to prevent unbounded recursion.
+   * @return {Promise<Array<Object>>} Array of Lando container descriptors.
    */
-  async list(options = {}, separator = '_') {
+  async list(options = {}, separator = '_', _retryCount = 0) {
     // Get raw container list from nerdctl (JSONL: one JSON object per line)
     let rawOutput;
     try {
@@ -380,7 +381,10 @@
     // If any container has been up for only a brief moment, retry
     // (matches Landerode behavior to avoid transient states)
     if (_.find(containers, container => container.status === 'Up Less than a second')) {
-      return this.list(options, separator);
+      if (_retryCount < 10) {
+        return this.list(options, separator, _retryCount + 1);
+      }
+      this.debug('list retry limit reached, proceeding with transient container states');
     }
 
     // Add running status flag
@@ -401,7 +405,7 @@
    * @param {Object} [opts={v: true, force: false}] - Removal options.
    * @param {boolean} [opts.v=true] - Also remove associated anonymous volumes.
    * @param {boolean} [opts.force=false] - Force-remove a running container.
-   * @returns {Promise<void>}
+   * @return {Promise<void>}
    */
   async remove(cid, opts = {v: true, force: false}) {
     const args = ['rm'];
@@ -428,7 +432,7 @@
    *
    * @param {string} cid - A container identifier.
    * @param {Object} [opts={}] - Stop options (e.g. `{t: 10}` for timeout in seconds).
-   * @returns {Promise<void>}
+   * @return {Promise<void>}
    */
   async stop(cid, opts = {}) {
     const args = ['stop'];
@@ -458,7 +462,7 @@
    * handle interface.
    *
    * @param {string} id - The network id or name.
-   * @returns {Object} A network handle with `inspect()` and `remove()` methods.
+   * @return {Object} A network handle with `inspect()` and `remove()` methods.
    */
   getNetwork(id) {
     return {
@@ -467,7 +471,7 @@
 
       /**
        * Inspect the network and return its metadata.
-       * @returns {Promise<Object>} Network inspect data.
+       * @return {Promise<Object>} Network inspect data.
        */
       inspect: async () => {
         const data = await this._nerdctl(['network', 'inspect', id]);
@@ -477,7 +481,7 @@
 
       /**
        * Remove the network.
-       * @returns {Promise<void>}
+       * @return {Promise<void>}
        */
       remove: async () => {
         try {
@@ -498,7 +502,7 @@
    *
    * @param {Object} [opts={}] - Filter options.
    * @param {Object} [opts.filters] - Filters object (e.g. `{name: ['mynet']}` or `{id: ['abc']}`).
-   * @returns {Promise<Array<Object>>} Array of network objects.
+   * @return {Promise<Array<Object>>} Array of network objects.
    */
   async listNetworks(opts = {}) {
     let rawOutput;
@@ -565,7 +569,7 @@
    * Dockerode Container handle interface.
    *
    * @param {string} cid - The container id or name.
-   * @returns {Object} A container handle with `inspect()`, `remove()`, and `stop()` methods.
+   * @return {Object} A container handle with `inspect()`, `remove()`, and `stop()` methods.
    */
   getContainer(cid) {
     return {
@@ -574,21 +578,21 @@
 
       /**
        * Inspect the container and return its metadata.
-       * @returns {Promise<Object>} Container inspect data.
+       * @return {Promise<Object>} Container inspect data.
        */
       inspect: () => this.scan(cid),
 
       /**
        * Remove the container.
        * @param {Object} [opts] - Removal options.
-       * @returns {Promise<void>}
+       * @return {Promise<void>}
        */
       remove: opts => this.remove(cid, opts),
 
       /**
        * Stop the container.
        * @param {Object} [opts] - Stop options.
-       * @returns {Promise<void>}
+       * @return {Promise<void>}
        */
       stop: opts => this.stop(cid, opts),
     };

diff --git a/lib/engine.js b/lib/engine.js
--- a/lib/engine.js
+++ b/lib/engine.js
@@ -194,7 +194,7 @@
     const semver = require('semver');
 
     // helper to normalize a supported versions object into comparison-ready format
-    const normalize = (sv) => _(sv)
+    const normalize = sv => _(sv)
       .map((data, name) => _.merge({}, data, {name}))
       .map(data => ([data.name, {
         satisfies: data.satisfies || `${data.min} - ${data.max}`,
@@ -207,12 +207,19 @@
 
     return this.daemon.getVersions().then(versions => {
       // Detect containerd backend: versions have containerd key instead of desktop/engine
-      const isContainerd = versions.hasOwnProperty('containerd');
+      const isContainerd = Object.prototype.hasOwnProperty.call(versions, 'containerd');
 
       let normalizedVersions;
       if (isContainerd) {
         // containerd format: {containerd, buildkit, nerdctl}
         normalizedVersions = normalize(this.supportedContainerdVersions);
+
+        // Remove false values (binaries that couldn't be versioned)
+        Object.keys(versions).forEach(key => {
+          if (versions[key] === false || versions[key] === 'skip') {
+            delete versions[key];
+          }
+        });
       } else {
         // Docker format: {desktop, engine, compose}
         normalizedVersions = normalize(supportedVersions);

diff --git a/messages/update-nerdctl-warning.js b/messages/update-nerdctl-warning.js
--- a/messages/update-nerdctl-warning.js
+++ b/messages/update-nerdctl-warning.js
@@ -1,13 +1,14 @@
 'use strict';
 
 // checks to see if a setting is disabled
-module.exports = () => ({
+module.exports = ({version, update, link} = {}) => ({
   type: 'warning',
   title: 'Recommend updating NERDCTL',
   detail: [
-    'Looks like you might be falling a bit behind on nerdctl.',
+    `You have version ${version || 'unknown'} but we recommend updating to ${update || 'the latest version'}.`,
     'In order to ensure the best stability and support we recommend you update',
     'by running the hidden "lando setup" command.',
   ],
   command: 'lando setup --skip-common-plugins',
+  url: link,
 });

cursoragent and others added 6 commits March 14, 2026 03:28
…ing, prevent recursion, filter false versions
New hook downloads Lima, creates a containerd-enabled VM during
'lando setup' on macOS. Platform-guarded in index.js.

Also fixes make-executable calls with absolute paths in both
darwin and containerd setup hooks, corrects Lima arch mapping
(arm64 → aarch64), and uses --plain for non-interactive VM creation.

Part of the containerd/nerdctl engine initiative.
Create utils/get-containerd-config.js for TOML config generation
on all platforms. Replaces WSL-specific config in wsl-helper.js.

Fixes: state/root as top-level scalars (not TOML tables),
disabled_plugins array for CRI, debug flag always-truthy bug.

17 new tests for config generation.

Part of the containerd/nerdctl engine initiative.
Create utils/get-buildkit-config.js for BuildKit TOML config generation.
Containerd worker with GC policy (reservedSpace), parallelism from CPU
count, optional registry mirrors and debug mode.

ContainerdDaemon now generates buildkit config, passes --config to
buildkitd, and exposes pruneBuildCache() via buildctl.

21 new tests. Config uses correct BuildKit field names per current docs.

Part of the containerd/nerdctl engine initiative.
- Add utils/setup-containerd-auth.js for Docker config path resolution,
  credential helper detection, and DOCKER_CONFIG env injection
- Update NerdctlCompose._transform() to inject auth env into commands
- Update ContainerdContainer._nerdctl() to pass auth env to nerdctl
- Add registryAuth config option for custom Docker config paths
- 23 new tests for auth config resolution and credential helpers
- Remove --internal flag from createNet (nerdctl doesn't support it)
- Set daemon.compose = nerdctlBin so Engine.composeInstalled works
- Add /usr/sbin:/sbin to compose function PATH for CNI plugins
- Add TODO for v4 image builds still using Docker buildx
- Update networking tests for --internal removal
The reset hook checks lando.config.orchestratorBin — when it points to
docker-compose and the engine is containerd, composeInstalled is false
and the hook replaces the entire engine with Docker's setup-engine.

Fix: update lando.config.orchestratorBin to nerdctlBin when engine is
containerd, so the reset hook sees compose as installed.
The app-reset-orchestrator hook replaces the engine with Docker's
setup-engine when composeInstalled is false. Skip this for containerd
since it manages its own compose backend via nerdctl.
app.js was always creating a fresh Docker engine via setup-engine.js,
bypassing the BackendManager entirely. Now uses lando.engine when
available (which contains the containerd compose function).
On WSL, ~/.docker/config.json may have credsStore: 'desktop.exe' from
Docker Desktop for Windows, but the helper binary doesn't exist in WSL.
This causes nerdctl to fail with 'unable to retrieve credentials'.

Fix: detect missing cred helper at auth config time, create a sanitized
config at ~/.lando/docker-config/ without the broken credsStore, and
set DOCKER_CONFIG to point there.
nerdctl treats credential helper errors as fatal (unlike Docker which
falls back to anonymous). Always remove credsStore from Docker config
for nerdctl, creating a sanitized copy at ~/.lando/docker-config/.
Also pass authConfig to NerdctlCompose so DOCKER_CONFIG propagates
to nerdctl compose subprocesses.
Rootless nerdctl can't auto-allocate host ports. Add port rewriting
that finds free ports and replaces bare specs like '127.0.0.1::80'
with '127.0.0.1:FREE_PORT:80' in compose files before nerdctl runs.

- utils/allocate-ports.js for port scanning and rewriting
- backend-manager.js pre-processes compose files in rootless mode
- 15 new tests for port allocation
Lando's router.js uses Bluebird-specific methods (.each, .tap, .map)
on return values from docker.list(), docker.isRunning(), etc. Our
async/await methods return native Promises. Wrap with a Proxy that
converts all Promise returns to Bluebird.
Replace rootless containerd (UID namespace issues) with rootful:
- Root-owned binaries at /usr/local/lib/lando/bin/ (containerd, shim,
  buildkitd, buildctl, runc) — prevents privilege escalation
- User-owned nerdctl stays at ~/.lando/bin/
- lando-containerd.service systemd unit runs containerd as root
- 'lando' group gets socket access (like docker group)
- lando setup handles sudo, group creation, service install
- Remove all rootless code (useRootless, _startRootless, port
  allocation rewriting, rootless detection)
nerdctl v2 refuses to work as non-root even with --address pointing
to a rootful socket. Instead, use docker-compose (already installed
by Lando) with DOCKER_HOST pointing to finch-daemon's Docker API
socket. finch-daemon translates to containerd.

Architecture: docker-compose → finch-daemon → containerd (rootful)

- Replace NerdctlCompose with lib/compose.js (same as Docker path)
- Set DOCKER_HOST=unix://~/.lando/run/finch.sock in compose env
- Use existing docker-compose binary as orchestratorBin
- NerdctlCompose class retained but no longer used for compose ops
Move containerd/finch sockets from ~/.lando/run/ (user-controlled) to
/run/lando/ (root-owned) to prevent symlink attacks. Same pattern as
Docker using /var/run/docker.sock.

- Systemd RuntimeDirectory=lando creates /run/lando/ automatically
- ExecStartPost sets lando group permissions on sockets
- PID files stay in ~/.lando/run/ (user-level)
- Download finch-daemon binary during lando setup (root-owned)
- Add ExecStartPost to systemd service that launches finch-daemon
  alongside containerd, with socket at /run/lando/finch.sock
- Service task depends on finch-daemon being installed
… in status checks

- Replace daemon.up() with passive daemon.isUp() in hasRun() for build engine
  hooks (linux, darwin, win32) and landonet hook. These were triggering full
  retry loops with socket polling just to check installation status.
- WSL remains the exception: docker binaries only appear after Docker Desktop
  starts on Windows, so a minimal daemon.up({max:1}) is still needed there.
- Short-circuit containerd up() and _waitForSocket() early if required binaries
  (containerd, nerdctl) don't exist, avoiding futile retry loops on fresh installs.
- Fix double parse-setup-task() mutation: getSetupStatus() now extracts task
  fields with inline defaults instead of calling parse-setup-task(), which
  mutates/wraps the task object. setup() remains the sole caller.
The Docker build engine setup hooks run unconditionally, causing
lando setup to try installing Docker even when engine=containerd.
Add guard to skip Docker install on all platforms when containerd
is the selected engine.
nerdctl refuses to work as non-root with rootful containerd. Replace
all nerdctl shell-outs with Dockerode API calls via finch-daemon socket.

- ContainerdContainer now uses Dockerode({socketPath: finchSocket})
- list(), scan(), isRunning(), remove(), stop() use Docker API
- createNet(), listNetworks() use Docker API
- getContainer/getNetwork proxies wrap Dockerode objects
- No more nerdctl dependency for any runtime operation
- nerdctl binary no longer required (only kept for version checks)

Architecture: Dockerode → finch-daemon socket → containerd (rootful)
- Landonet setup now depends on setup-containerd-service when engine
  is containerd (was depending on setup-build-engine/Docker)
- Fix orchestratorBin to point to docker-compose (not nerdctl) when
  containerd engine is active
- Skip Docker Desktop binary check for containerd in landonet hasRun
The old service was 'enabled' but didn't have finch-daemon or /run/lando/
paths. hasRun returned true, skipping the service update. Now also
verifies both sockets exist at /run/lando/ before considering done.
When the service is already running with old config, 'systemctl start'
is a no-op. Need 'restart' to pick up the new service file with
finch-daemon and /run/lando/ socket paths.
finch-daemon adds unix:// internally. Passing unix:///run/lando/finch.sock
causes it to try 'unix://unix:///run/lando/finch.sock'. Just pass the
bare path.
finch-daemon/nerdctl needs /etc/cni/net.d/ for network lock files
and /opt/cni/bin/ for CNI plugins. Create them before containerd starts.
Users shouldn't need manual commands. Create /etc/cni/net.d and
/opt/cni/bin during the setup task with sudo, before restarting
the service.
finch-daemon uses nerdctl internally which defaults to /run/containerd/
containerd.sock. Need to point it at our socket /run/lando/containerd.sock
via CONTAINERD_ADDRESS env var.
finch-daemon internally uses nerdctl which looks for containerd at
/run/containerd/containerd.sock. Symlink our socket there so
finch-daemon finds it without custom config.
isUp() and _healthCheck() used nerdctl ps which fails with rootless
error. Replace with Dockerode.ping() against finch-daemon socket —
same pattern as all other container ops.
Remove the /run/containerd/containerd.sock symlink hack. Instead,
write a nerdctl.toml config pointing to our socket and set NERDCTL_TOML
env var when starting finch-daemon. No conflict with system containerd.
finch-daemon expects CNI plugins at /opt/cni/bin/ but Ubuntu installs
them at /usr/lib/cni/. Symlink in ExecStartPre. Also add cni_path to
nerdctl.toml config.
…pping

docker-compose on WSL detects Docker Desktop and translates bind mount
paths through /run/desktop/mnt/host/wsl/docker-desktop-bind-mounts/.
Set DOCKER_CONTEXT=default to prevent this when using containerd.
containerd needs runc in PATH. Our runc is at /usr/local/lib/lando/bin/.
Add Environment=PATH with our bin dir to the systemd service.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants