Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
f1820f7
feat: add engine backend interface for multi-engine support
AaronFeledy Mar 13, 2026
9d73794
feat: add Docker backend wrapping existing engine code
AaronFeledy Mar 13, 2026
18ecd05
feat: add containerd daemon manager
AaronFeledy Mar 13, 2026
c0bbb00
feat: add containerd container manager (nerdctl wrapper)
AaronFeledy Mar 13, 2026
cad27e7
feat: add nerdctl compose adapter
AaronFeledy Mar 13, 2026
40000b1
feat: add backend selection and configuration
AaronFeledy Mar 13, 2026
c623d21
feat: add binary management for containerd/nerdctl/buildkit
AaronFeledy Mar 13, 2026
1952476
feat: add tests and docs for containerd engine support
AaronFeledy Mar 13, 2026
6c05b2d
feat: wire BackendManager into lando.js bootstrap
AaronFeledy Mar 14, 2026
270c369
feat: add lando setup hooks for containerd binaries
AaronFeledy Mar 14, 2026
9eff2a6
feat: add Lima VM integration for macOS containerd support
AaronFeledy Mar 14, 2026
c8c5a14
feat: update Engine for containerd version checking
AaronFeledy Mar 14, 2026
36923bd
feat: register containerd setup hooks in index.js
AaronFeledy Mar 14, 2026
980f6bd
feat: add containerd integration tests
AaronFeledy Mar 14, 2026
b064ca0
feat: add containerd compatibility check hooks
AaronFeledy Mar 14, 2026
05d105d
feat: add WSL2 containerd isolation support
AaronFeledy Mar 14, 2026
da24125
feat: register containerd compat hooks and verify docker proxy
AaronFeledy Mar 14, 2026
9dca7db
feat: add containerd version info hook
AaronFeledy Mar 14, 2026
5c38568
feat: add containerd smoke test script
AaronFeledy Mar 14, 2026
6ed6c1a
fix: address branch review findings
AaronFeledy Mar 14, 2026
abd2ef0
Fix backend logic bugs: add default opts, accept version info in warn…
cursoragent Mar 14, 2026
440b97e
docs: add containerd engine todo list (tasks 22-31)
AaronFeledy Mar 14, 2026
e6d3afe
feat: add Lima setup hook for macOS lando setup
AaronFeledy Mar 14, 2026
7729c16
feat: add shared containerd config generator
AaronFeledy Mar 14, 2026
63516bb
feat: add BuildKit config and cache management
AaronFeledy Mar 14, 2026
1b395e6
feat: add registry authentication support for containerd/nerdctl
AaronFeledy Mar 14, 2026
09b9a7e
Fix backend logic bugs: add default opts, accept version info in warn…
cursor[bot] Mar 14, 2026
854cb9f
feat: add volume mount compatibility layer for containerd
AaronFeledy Mar 14, 2026
1be7349
feat: add networking parity for containerd/nerdctl backend
AaronFeledy Mar 14, 2026
22f5771
feat: add finch-daemon for Docker API compatibility with containerd
AaronFeledy Mar 14, 2026
4e2e99e
feat: add engine selection UX and containerd doctor checks
AaronFeledy Mar 14, 2026
5784ca6
feat: add containerd error messages and troubleshooting
AaronFeledy Mar 14, 2026
678de49
feat: add performance benchmarking and perf logging for containerd
AaronFeledy Mar 14, 2026
9f53c2d
ci: run Leia tests against both docker and containerd engines
AaronFeledy Mar 14, 2026
a0e6931
Fix containerd integration bugs
cursoragent Mar 14, 2026
73a5b28
fix: nerdctl tarball entry path (root, not bin/)
AaronFeledy Mar 14, 2026
f87eb45
fix: TOML config structure — top-level keys before sections
AaronFeledy Mar 14, 2026
82cfe1b
fix: containerd setup and runtime fixes for functional parity
AaronFeledy Mar 14, 2026
b41d2b6
feat: rootless containerd support and functional runtime
AaronFeledy Mar 14, 2026
bc4b1d4
fix: runtime bugs for containerd engine parity
AaronFeledy Mar 14, 2026
a73dfd5
fix: prevent lando-reset-orchestrator from replacing containerd engine
AaronFeledy Mar 14, 2026
525fb9a
fix: skip app-level orchestrator reset for containerd engine
AaronFeledy Mar 14, 2026
bcca717
fix: app.js must use lando.engine instead of creating its own
AaronFeledy Mar 14, 2026
513f08c
fix: sanitize Docker config when credsStore helper is missing
AaronFeledy Mar 14, 2026
abf6ce8
fix: sanitize credsStore for nerdctl and pass auth to compose
AaronFeledy Mar 14, 2026
31c853c
feat: rootless port allocation for nerdctl compose
AaronFeledy Mar 15, 2026
5f2f969
fix: wrap ContainerdContainer returns in Bluebird promises
AaronFeledy Mar 15, 2026
7de2358
feat: switch to rootful containerd with systemd service
AaronFeledy Mar 15, 2026
c1dc204
feat: use docker-compose + finch-daemon instead of nerdctl compose
AaronFeledy Mar 15, 2026
c376f7c
feat: move sockets to /run/lando/ (root-owned, group-accessible)
AaronFeledy Mar 15, 2026
07014b3
fix: add finch-daemon to setup and systemd service
AaronFeledy Mar 15, 2026
5df6efa
perf: speed up lando setup by eliminating expensive daemon.up() calls…
AaronFeledy Mar 15, 2026
c41b5c0
fix: skip Docker Engine install when engine is containerd
AaronFeledy Mar 15, 2026
e43c728
feat: replace nerdctl with Dockerode for container operations
AaronFeledy Mar 15, 2026
7a68b80
fix: landonet depends on containerd service, fix orchestratorBin
AaronFeledy Mar 15, 2026
269033f
fix: service hasRun checks finch.sock + containerd.sock existence
AaronFeledy Mar 15, 2026
c08f532
fix: use systemctl restart (not start) for service update
AaronFeledy Mar 15, 2026
0a2d87c
fix: remove unix:// prefix from finch-daemon socket-addr
AaronFeledy Mar 15, 2026
afb01b9
fix: create CNI config dirs in systemd ExecStartPre
AaronFeledy Mar 15, 2026
2444814
fix: create CNI dirs during setup task, not just ExecStartPre
AaronFeledy Mar 15, 2026
d8ccba7
fix: create /etc/cni/net.d/finch subdirectory for finch-daemon
AaronFeledy Mar 15, 2026
e4fffb7
fix: set CONTAINERD_ADDRESS for finch-daemon in systemd service
AaronFeledy Mar 15, 2026
d78dd06
fix: symlink containerd socket to default path for finch-daemon
AaronFeledy Mar 15, 2026
da922de
fix: replace nerdctl health check with Dockerode ping via finch
AaronFeledy Mar 15, 2026
e1639d3
fix: use NERDCTL_TOML for finch-daemon instead of socket symlink
AaronFeledy Mar 15, 2026
47ef985
fix: hasRun checks for nerdctl.toml to detect config changes
AaronFeledy Mar 15, 2026
4a56845
fix: symlink CNI plugins from /usr/lib/cni to /opt/cni/bin
AaronFeledy Mar 15, 2026
f774732
fix: set DOCKER_CONTEXT=default to prevent Docker Desktop WSL path ma…
AaronFeledy Mar 15, 2026
a1ce102
fix: add system bin dir to PATH in systemd service for runc
AaronFeledy Mar 15, 2026
232ad8f
feat(containerd): engine setup, hooks, tests, and utilities
AaronFeledy Mar 21, 2026
1eaad1b
fix(containerd): remove nerdctl from user-facing code paths and fix a…
AaronFeledy Mar 21, 2026
af2f1e8
feat(containerd): add Traefik proxy compatibility with containerd bac…
AaronFeledy Mar 21, 2026
34db526
feat(containerd): isolate CNI paths, fix permissions, add troubleshoo…
AaronFeledy Mar 27, 2026
af1c4e3
feat(containerd): ensure CNI configs for all compose-defined networks…
AaronFeledy Mar 27, 2026
4d63559
fix(containerd): fix binary path check, add test coverage, deprecate …
AaronFeledy Mar 28, 2026
d4112de
test(containerd): add LimaManager/WslHelper unit tests, update smoke …
AaronFeledy Mar 28, 2026
fa7043c
fix(containerd): fix outbound internet, ephemeral state dir, OCI hook…
AaronFeledy Mar 28, 2026
dab5b91
fix(containerd): multi-container orchestration — fix finch-daemon net…
AaronFeledy Mar 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/pr-containerd-tests.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
name: Containerd Engine Tests

on:
pull_request:

jobs:
leia-tests:
runs-on: ${{ matrix.os }}
env:
TERM: xterm
strategy:
fail-fast: false
matrix:
leia-test:
- containerd
node-version:
- "20"
os:
- ubuntu-24.04

steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install node ${{ matrix.node-version }}
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
registry-url: https://registry.npmjs.org
cache: npm
- name: Bundle Deps
uses: lando/prepare-release-action@v3
with:
lando-plugin: true
version: dev
sync: false
- name: Install pkg dependencies
run: npm clean-install --prefer-offline --frozen-lockfile --production
- name: Package into node binary
uses: lando/pkg-action@v6
id: pkg-action
with:
entrypoint: bin/lando
filename: lando
node-version: ${{ matrix.node-version }}
options: --options dns-result-order=ipv4first
upload: false
pkg: "@yao-pkg/pkg@5.16.1"
- name: Install full deps
run: npm clean-install --prefer-offline --frozen-lockfile
- name: Setup lando ${{ steps.pkg-action.outputs.file }}
uses: lando/setup-lando@v3
with:
auto-setup: false
lando-version: ${{ steps.pkg-action.outputs.file }}
telemetry: false
- name: Run Leia Tests
uses: lando/run-leia-action@v2
with:
leia-test: "./examples/${{ matrix.leia-test }}/README.md"
cleanup-header: "Destroy tests"
shell: bash
stdin: true
6 changes: 5 additions & 1 deletion .github/workflows/pr-core-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ on:

jobs:
leia-tests:
name: ${{ matrix.leia-test }} (${{ matrix.engine }})
runs-on: ${{ matrix.os }}
env:
TERM: xterm
Expand Down Expand Up @@ -62,6 +63,8 @@ jobs:
- update
- version
- yaml
engine:
- docker
node-version:
- "20"
os:
Expand Down Expand Up @@ -106,13 +109,14 @@ jobs:
pkg: "@yao-pkg/pkg@5.16.1"
- name: Install full deps
run: npm clean-install --prefer-offline --frozen-lockfile
- name: Setup lando ${{ steps.pkg-action.outputs.file }}
- name: Setup lando ${{ steps.pkg-action.outputs.file }} (${{ matrix.engine }})
uses: lando/setup-lando@v3
with:
lando-version: ${{ steps.pkg-action.outputs.file }}
telemetry: false
config: |
setup.skipCommonPlugins=true
engine=${{ matrix.engine }}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Config YAML indentation breaks engine setting in CI

High Severity

The config parameter for lando/setup-lando uses a YAML multi-line block (|), but engine=${{ matrix.engine }} on line 120 lacks the leading whitespace indentation to be on its own line within the block scalar. It appears to continue after setup.skipCommonPlugins=true without a newline separator, resulting in a malformed config value like setup.skipCommonPlugins=trueengine=docker instead of two separate config entries.

Fix in Cursor Fix in Web

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bugbot Autofix determined this is a false positive.

Both config lines have identical 12-space indentation and parse correctly as a valid YAML block scalar.

- name: Run Leia Tests
uses: lando/run-leia-action@v2
env:
Expand Down
228 changes: 228 additions & 0 deletions BRIEF.md

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,9 @@ module.exports = async (app, lando) => {
// Check for docker compat warnings and surface them nicely as well
app.events.on('post-start', async () => await require('./hooks/app-check-docker-compat')(app, lando));

// Check for containerd compat warnings and surface them nicely as well
app.events.on('post-start', async () => await require('./hooks/app-check-containerd-compat')(app, lando));

// throw service not start errors
app.events.on('post-start', 1, async () => await require('./hooks/app-check-v4-service-running')(app, lando));

Expand Down
4 changes: 2 additions & 2 deletions builders/_proxy.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const _ = require('lodash');
/*
* Helper to get core proxy service
*/
const getProxy = ({proxyCommand, proxyPassThru, proxyDomain, userConfRoot, version = 'unknown'} = {}) => {
const getProxy = ({proxyCommand, proxyPassThru, proxyDomain, userConfRoot, dockerSocket, version = 'unknown'} = {}) => {
return {
services: {
proxy: {
Expand All @@ -21,7 +21,7 @@ const getProxy = ({proxyCommand, proxyPassThru, proxyDomain, userConfRoot, versi
},
networks: ['edge'],
volumes: [
'/var/run/docker.sock:/var/run/docker.sock',
`${dockerSocket || '/var/run/docker.sock'}:/var/run/docker.sock`,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Proxy builder never receives dockerSocket for containerd

Medium Severity

The proxy builder now accepts dockerSocket and uses it for the Traefik volume mount, but the caller at line 73 passes options which comes from the proxy configuration. There's no code in index.js or anywhere in the diff that actually sets dockerSocket in the proxy options when the containerd backend is active, so the finch-daemon socket path will never reach the proxy. Traefik will try to connect to /var/run/docker.sock which won't exist under containerd.

Fix in Cursor Fix in Web

`${userConfRoot}/scripts/proxy-certs.sh:/scripts/100-proxy-certs`,
'proxy_config:/proxy_config',
],
Expand Down
6 changes: 5 additions & 1 deletion builders/lando-v4.js
Original file line number Diff line number Diff line change
Expand Up @@ -434,12 +434,16 @@ module.exports = {
}, config.labels);

// add it all 2getha
const networks = lando.engine?.engineBackend === 'containerd'
? {}
: {[this.network]: {aliases: this.hostnames}};

this.addLandoServiceData({
environment,
extra_hosts: ['host.lando.internal:host-gateway'],
labels,
logging: {driver: 'json-file', options: {'max-file': '3', 'max-size': '10m'}},
networks: {[this.network]: {aliases: this.hostnames}},
networks,
user: this.user.name,
volumes: this.volumes,
});
Expand Down
125 changes: 107 additions & 18 deletions components/docker-engine.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

const fs = require('fs-extra');
const path = require('path');

const merge = require('lodash/merge');
const slugify = require('slugify');

Expand Down Expand Up @@ -35,7 +36,21 @@ class DockerEngine extends Dockerode {
orchestrator = DockerEngine.orchestrator,
} = {}) {
super(config);
this.builder = builder;
const userConfRoot = config.userConfRoot || path.join(require('os').homedir(), '.lando');
const systemBinDir = config.containerdSystemBinDir || '/usr/local/lib/lando/bin';

this.containerdMode = config.containerdMode === true
|| config.engine === 'containerd'
|| process.env.LANDO_ENGINE === 'containerd';
this.containerdNamespace = config.containerdNamespace || 'default';
this.containerdSocket = config.containerdSocket || '/run/lando/containerd.sock';
this.buildkitHost = config.buildkitHost || 'unix:///run/lando/buildkitd.sock';
this.buildctl = config.buildctlBin
|| (fs.existsSync(path.join(userConfRoot, 'bin', 'buildctl')) ? path.join(userConfRoot, 'bin', 'buildctl') : path.join(systemBinDir, 'buildctl'));
this.nerdctlConfig = config.nerdctlConfig || path.join(userConfRoot, 'config', 'nerdctl.toml');
this.authConfig = config.authConfig || {env: {}};
this.builder = this.containerdMode ? path.join(userConfRoot, 'bin', 'nerdctl') : builder;
if (this.containerdMode) this.modem.socketPath = config.socketPath || '/run/lando/finch.sock';
this.debug = debug;
this.orchestrator = orchestrator;
}
Expand All @@ -56,6 +71,10 @@ class DockerEngine extends Dockerode {
id = tag,
sources = [],
} = {}) {
if (this.containerdMode) {
return this.buildx(dockerfile, {attach, buildArgs, context, id, sources, tag});
}

// handles the promisification of the merged return
const awaitHandler = async () => {
return new Promise((resolve, reject) => {
Expand Down Expand Up @@ -244,21 +263,27 @@ class DockerEngine extends Dockerode {
fs.copySync(dockerfile, path.join(context, 'Dockerfile'));
dockerfile = path.join(context, 'Dockerfile');

// build initial buildx command
const args = {
command: this.builder,
args: [
'buildx',
'build',
`--file=${dockerfile}`,
'--progress=plain',
`--tag=${tag}`,
context,
],
};
const outputPath = this.containerdMode ? path.join(context, 'image.tar') : null;

// build initial build command
const args = this.containerdMode
? this._getContainerdBuildctlCommand({buildArgs, context, dockerfile, outputPath, tag})
: {
command: this.builder,
args: [
'buildx',
'build',
`--file=${dockerfile}`,
'--progress=plain',
`--tag=${tag}`,
context,
],
};

// add any needed build args into the command
for (const [key, value] of Object.entries(buildArgs)) args.args.push(`--build-arg=${key}=${value}`);
if (!this.containerdMode) {
for (const [key, value] of Object.entries(buildArgs)) args.args.push(`--build-arg=${key}=${value}`);
}

// if we have sshKeys then lets pass those in
if (sshKeys.length > 0) {
Expand All @@ -274,15 +299,16 @@ class DockerEngine extends Dockerode {

// if we have an sshAuth socket then add that as well
if (sshSocket && fs.existsSync(sshSocket)) {
args.args.push(`--ssh=agent=${sshSocket}`);
args.args.push(`--ssh=${this.containerdMode ? 'default' : 'agent'}=${sshSocket}`);
debug('passing in ssh agent socket %o', sshSocket);
}

// get builder
// @TODO: consider other opts? https://docs.docker.com/reference/cli/docker/buildx/build/ args?
// secrets?
// gha cache-from/to?
const buildxer = require('../utils/run-command')(args.command, args.args, {debug});
const env = {...process.env, ...(this.authConfig.env || {})};
const buildxer = require('../utils/run-command')(args.command, args.args, {debug, env});

// augment buildxer with more events so it has the same interface as build
buildxer.stdout.on('data', data => {
Expand All @@ -297,25 +323,88 @@ class DockerEngine extends Dockerode {
for (const line of data.toString().trim().split('\n')) debug(line);
stderr += data;
});
buildxer.on('close', code => {
buildxer.on('close', async code => {
// if code is non-zero and we arent ignoring then reject here
if (code !== 0 && !ignoreReturnCode) {
buildxer.emit('error', require('../utils/get-buildx-error')({code, stdout, stderr}));
// otherwise return done
} else {
try {
if (this.containerdMode && outputPath) {
const loadOutput = await this._loadContainerdImage(outputPath, tag, debug);
stdout += loadOutput;
}
} catch (error) {
buildxer.emit('error', error);
return;
} finally {
if (outputPath && fs.existsSync(outputPath)) fs.removeSync(outputPath);
}

buildxer.emit('done', {code, stdout, stderr});
buildxer.emit('finished', {code, stdout, stderr});
buildxer.emit('success', {code, stdout, stderr});
}
});

// debug
debug('buildxing image %o from %o with build-args', tag, context, buildArgs);
debug('%s image %o from %o with build-args %o', this.containerdMode ? 'building with buildctl' : 'buildxing', tag, context, buildArgs);

// return merger
return mergePromise(buildxer, awaitHandler);
}

_getContainerdBuildctlCommand({buildArgs = {}, context, dockerfile, outputPath, tag}) {
const filename = path.basename(dockerfile);
const args = [
'--addr', this.buildkitHost,
'build',
'--frontend', 'dockerfile.v0',
'--local', `context=${context}`,
'--local', `dockerfile=${path.dirname(dockerfile)}`,
'--opt', `filename=${filename}`,
'--opt', `platform=${process.arch === 'arm64' ? 'linux/arm64' : 'linux/amd64'}`,
'--output', `type=docker,name=${tag},dest=${outputPath}`,
'--progress=plain',
];

for (const [key, value] of Object.entries(buildArgs)) args.push('--opt', `build-arg:${key}=${value}`);

return {
command: this.buildctl,
args,
};
}

async _loadContainerdImage(imageTarball, tag, debug = this.debug) {
// Load via finch-daemon's Docker-compatible API (Dockerode).
// finch-daemon proxies to containerd, so this loads into both.
return this._loadContainerdImageIntoFinch(imageTarball, tag, debug);
}

async _loadContainerdImageIntoFinch(imageTarball, tag, debug = this.debug) {
return new Promise((resolve, reject) => {
const stream = fs.createReadStream(imageTarball);

this.loadImage(stream, (error, responseStream) => {
if (error) return reject(error);
if (!responseStream) return resolve('');

this.modem.followProgress(responseStream, (followError, output = []) => {
if (followError) return reject(followError);

const messages = output
.map(event => event.stream || event.status || '')
.filter(Boolean);

for (const message of messages) debug(message.trim());
debug('loaded image %o into finch-daemon from %o', tag, imageTarball);
resolve(messages.join(''));
});
});
});
}

/*
* A helper method that automatically will build the image needed for the run command
* NOTE: this is only available as async/await so you cannot return directly and access events
Expand Down
Loading
Loading