-
-
Notifications
You must be signed in to change notification settings - Fork 41
feat: containerd/nerdctl engine backend #445
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 20 commits
f1820f7
9d73794
18ecd05
c0bbb00
cad27e7
40000b1
c623d21
1952476
6c05b2d
270c369
9eff2a6
c8c5a14
36923bd
980f6bd
b064ca0
05d105d
da24125
9dca7db
5c38568
6ed6c1a
abd2ef0
440b97e
e6d3afe
7729c16
63516bb
1b395e6
09b9a7e
854cb9f
1be7349
22f5771
4e2e99e
5784ca6
678de49
9f53c2d
a0e6931
73a5b28
f87eb45
82cfe1b
b41d2b6
bc4b1d4
a73dfd5
525fb9a
bcca717
513f08c
abf6ce8
31c853c
5f2f969
7de2358
c1dc204
c376f7c
07014b3
5df6efa
c41b5c0
e43c728
7a68b80
269033f
c08f532
0a2d87c
afb01b9
2444814
d8ccba7
e4fffb7
d78dd06
da922de
e1639d3
47ef985
4a56845
f774732
a1ce102
232ad8f
1eaad1b
af2f1e8
34db526
af1c4e3
4d63559
d4112de
fa7043c
dab5b91
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,89 @@ | ||
| --- | ||
| title: Engine | ||
| description: How to configure the Lando container engine backend (Docker or containerd) | ||
| --- | ||
|
|
||
| # Engine | ||
|
|
||
| Lando supports multiple container engine backends. By default it uses [Docker](https://www.docker.com/) but can also use [containerd](https://containerd.io/) with [nerdctl](https://github.com/containerd/nerdctl) as an alternative runtime. | ||
|
|
||
| The engine backend can be configured via the `engine` key in the [global config](global.md) or per-project in your `.lando.yml`. | ||
|
|
||
| ## Available Values | ||
|
|
||
| | Value | Description | | ||
| |---|---| | ||
| | `auto` | **(default)** Auto-detects the best available backend. Prefers containerd if all binaries are found, otherwise falls back to Docker. | | ||
| | `docker` | Always use the Docker daemon and Docker Compose. This is the traditional Lando behavior. | | ||
| | `containerd` | Use Lando's own isolated containerd + buildkitd + nerdctl stack. | | ||
|
|
||
| ## Configuration | ||
|
|
||
| **Global config (~/.lando/config.yml)** | ||
|
|
||
| ```yaml | ||
| # use auto-detection (default) | ||
| engine: auto | ||
|
|
||
| # force Docker | ||
| engine: docker | ||
|
|
||
| # force containerd | ||
| engine: containerd | ||
| ``` | ||
|
|
||
| **Per-project (.lando.yml)** | ||
|
|
||
| ```yaml | ||
| name: my-app | ||
| engine: containerd | ||
| services: | ||
| web: | ||
| type: php:8.2 | ||
| via: nginx | ||
| ``` | ||
|
|
||
| ## Auto-Detection | ||
|
|
||
| When `engine` is set to `auto` (the default), Lando checks for the presence of three binaries inside `~/.lando/bin/`: | ||
|
|
||
| 1. `containerd` — the container runtime daemon | ||
| 2. `nerdctl` — the Docker-compatible CLI for containerd | ||
| 3. `buildkitd` — the image build daemon | ||
|
|
||
| If **all three** binaries exist, Lando uses the containerd backend. If any are missing, it falls back to Docker. | ||
|
|
||
| ::: tip | ||
| The containerd binaries are installed automatically by `lando setup` when containerd support is enabled. You don't need to install them manually. | ||
| ::: | ||
|
|
||
| ## Overriding Binary Paths | ||
|
|
||
| If your containerd stack binaries are installed in a non-standard location, you can override each path individually in the [global config](global.md): | ||
|
|
||
| ```yaml | ||
| # Override individual binary paths | ||
| containerdBin: /usr/local/bin/containerd | ||
| nerdctlBin: /usr/local/bin/nerdctl | ||
| buildkitdBin: /usr/local/bin/buildkitd | ||
|
|
||
| # Override the containerd socket path | ||
| containerdSocket: /run/containerd/containerd.sock | ||
| ``` | ||
|
|
||
| By default, Lando looks for binaries in `~/.lando/bin/` and manages its own isolated containerd socket at `~/.lando/run/containerd.sock`. | ||
|
|
||
| ## How It Works | ||
|
|
||
| When using the containerd backend, Lando: | ||
|
|
||
| 1. Starts its **own isolated** containerd and buildkitd daemons (separate from any system containerd) | ||
| 2. Uses `nerdctl compose` instead of `docker compose` for service orchestration | ||
| 3. Uses `nerdctl` instead of `docker` for container inspection, listing, and management | ||
| 4. Manages all state in `~/.lando/` to avoid interfering with system containers | ||
|
|
||
| The containerd backend is fully compatible with existing Lando apps and compose files — no changes to your `.lando.yml` services are required. | ||
|
|
||
| ::: warning EXPERIMENTAL | ||
| The containerd engine backend is experimental. While it is designed to be a drop-in replacement for the Docker backend, some edge cases may behave differently. Please report any issues you encounter. | ||
| ::: |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| 'use strict'; | ||
|
|
||
| const _ = require('lodash'); | ||
|
|
||
| module.exports = async (app, lando) => { | ||
| // Skip if not using the containerd backend | ||
| const backend = _.get(lando, 'engine.engineBackend', _.get(lando, 'config.engine', 'auto')); | ||
| if (backend !== 'containerd') return; | ||
|
|
||
| _.forEach(_(lando.versions).filter(version => version && !version.dockerVersion).value(), thing => { | ||
| // handle generic unsupported or untested notices | ||
| if (!thing.satisfied) app.addMessage(require('../messages/unsupported-version-warning')({ | ||
|
Check failure on line 12 in hooks/app-check-containerd-compat.js
|
||
| ...thing, | ||
| name: thing.name, | ||
| })); | ||
| if (thing.untested) app.addMessage(require('../messages/untested-version-notice')(thing)); | ||
|
|
||
| // handle nerdctl (compose equivalent) recommend update | ||
| if (thing.name === 'nerdctl' && thing.rupdate) { | ||
| app.addMessage(require('../messages/update-nerdctl-warning')(thing)); | ||
| } | ||
| }); | ||
|
|
||
| // Run live containerd-specific health checks | ||
| try { | ||
| const daemon = lando.engine.daemon; | ||
|
|
||
| // Verify containerd daemon is running | ||
| const isUp = await daemon.isUp(); | ||
| if (!isUp) { | ||
| app.addMessage({ | ||
| type: 'warning', | ||
| title: 'Containerd daemon is not running', | ||
| detail: [ | ||
| 'The containerd daemon does not appear to be running.', | ||
| 'Lando needs containerd to manage containers. Try running "lando start"', | ||
| 'which will attempt to start the daemon automatically.', | ||
| ], | ||
| }); | ||
| } | ||
|
|
||
| // Verify nerdctl compose is functional | ||
| if (isUp) { | ||
| try { | ||
| const runCommand = require('../utils/run-command'); | ||
| await runCommand(daemon.nerdctlBin, ['compose', 'version'], { | ||
| debug: daemon.debug, | ||
| ignoreReturnCode: false, | ||
| }); | ||
| } catch (err) { | ||
| app.addMessage({ | ||
| type: 'warning', | ||
| title: 'nerdctl compose is not functional', | ||
| detail: [ | ||
| 'Could not run "nerdctl compose version" successfully.', | ||
| 'nerdctl compose is required for service orchestration.', | ||
| `Error: ${err.message}`, | ||
| ], | ||
| url: 'https://github.com/containerd/nerdctl/releases', | ||
| }); | ||
| } | ||
|
|
||
| // Verify buildkitd is running | ||
| const buildkitRunning = daemon._isProcessRunning | ||
| ? daemon._isProcessRunning(daemon.buildkitdPidFile) | ||
| : false; | ||
|
|
||
| if (!buildkitRunning) { | ||
| app.addMessage({ | ||
| type: 'warning', | ||
| title: 'BuildKit daemon is not running', | ||
| detail: [ | ||
| 'The BuildKit daemon (buildkitd) does not appear to be running.', | ||
| 'BuildKit is required for building container images with the containerd backend.', | ||
| 'Try running "lando start" which will attempt to start buildkitd automatically.', | ||
| ], | ||
| url: 'https://github.com/moby/buildkit/releases', | ||
| }); | ||
| } | ||
| } | ||
| } catch (err) { | ||
| lando.log.debug('containerd health check encountered an error: %s', err.message); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,26 @@ | ||
| "use strict"; | ||
|
Check failure on line 1 in hooks/lando-add-containerd-version-info.js
|
||
|
|
||
| module.exports = async lando => { | ||
| // Only run for containerd backend | ||
| if (!lando.engine || lando.engine.engineBackend !== "containerd") return; | ||
|
Check failure on line 5 in hooks/lando-add-containerd-version-info.js
|
||
|
|
||
| try { | ||
| const versions = await lando.engine.daemon.getVersions(); | ||
| lando.log.debug("containerd versions: %o", versions); | ||
|
Check failure on line 9 in hooks/lando-add-containerd-version-info.js
|
||
|
|
||
| // Add to lando.versions alongside any Docker version info | ||
| if (!lando.versions) lando.versions = []; | ||
|
|
||
| for (const [name, version] of Object.entries(versions)) { | ||
| if (!version) continue; | ||
| lando.versions.push({ | ||
| name, | ||
| version, | ||
| dockerVersion: false, | ||
| satisfied: true, | ||
| }); | ||
| } | ||
| } catch (err) { | ||
| lando.log.warn("could not retrieve containerd version info: %s", err.message); | ||
|
Check failure on line 24 in hooks/lando-add-containerd-version-info.js
|
||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| 'use strict'; | ||
|
|
||
| const _ = require('lodash'); | ||
|
|
||
| module.exports = async lando => { | ||
| // only run if engine bootstrap or above, containerd backend, and daemon is available | ||
| if (lando._bootstrapLevel >= 3) { | ||
| const backend = _.get(lando, 'engine.engineBackend', _.get(lando, 'config.engine', 'auto')); | ||
| if (backend === 'containerd' && lando.engine.dockerInstalled) { | ||
| lando.engine.getCompatibility().then(results => { | ||
| lando.log.verbose('checking containerd version compatibility...'); | ||
| lando.log.debug('containerd compatibility results', _.keyBy(results, 'name')); | ||
| lando.cache.set('versions', _.assign(lando.versions, _.keyBy(results, 'name')), {persist: true}); | ||
| lando.versions = lando.cache.get('versions'); | ||
| }); | ||
| } | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| "use strict"; | ||
|
Check failure on line 1 in hooks/lando-setup-containerd-engine-check.js
|
||
|
|
||
| const fs = require("fs"); | ||
|
Check failure on line 3 in hooks/lando-setup-containerd-engine-check.js
|
||
| const os = require("os"); | ||
|
Check failure on line 4 in hooks/lando-setup-containerd-engine-check.js
|
||
| const path = require("path"); | ||
|
Check failure on line 5 in hooks/lando-setup-containerd-engine-check.js
|
||
|
|
||
| module.exports = async (lando) => { | ||
|
Check failure on line 7 in hooks/lando-setup-containerd-engine-check.js
|
||
| const engine = lando.config.engine || "auto"; | ||
| // Only check when engine is explicitly containerd | ||
| if (engine !== "containerd") return; | ||
|
|
||
| const userConfRoot = lando.config.userConfRoot || path.join(os.homedir(), ".lando"); | ||
| const binDir = path.join(userConfRoot, "bin"); | ||
|
|
||
| const missing = []; | ||
| const bins = { | ||
| containerd: lando.config.containerdBin || path.join(binDir, "containerd"), | ||
| nerdctl: lando.config.nerdctlBin || path.join(binDir, "nerdctl"), | ||
| buildkitd: lando.config.buildkitdBin || path.join(binDir, "buildkitd"), | ||
| }; | ||
|
|
||
| for (const [name, binPath] of Object.entries(bins)) { | ||
| if (!fs.existsSync(binPath)) missing.push(name); | ||
| } | ||
|
|
||
| if (missing.length > 0) { | ||
| lando.log.warn( | ||
| "containerd engine selected but missing binaries: %s. Run \"lando setup\" to install them.", | ||
| missing.join(", "), | ||
| ); | ||
| } | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| "use strict"; | ||
|
|
||
| const fs = require("fs"); | ||
| const os = require("os"); | ||
| const path = require("path"); | ||
|
|
||
| module.exports = async (lando, options) => { | ||
| const debug = require("../utils/debug-shim")(lando.log); | ||
| const {color} = require("listr2"); | ||
| const getUrl = require("../utils/get-containerd-download-url"); | ||
| const axios = require("../utils/get-axios")(); | ||
|
|
||
| // Only run for containerd or auto engine selection | ||
| const engine = lando.config.engine || "auto"; | ||
| if (engine === "docker") return; | ||
|
|
||
| const userConfRoot = lando.config.userConfRoot || path.join(os.homedir(), ".lando"); | ||
| const binDir = path.join(userConfRoot, "bin"); | ||
|
|
||
| // Binary definitions | ||
| const binaries = [ | ||
| { | ||
| name: "containerd", | ||
| id: "setup-containerd", | ||
| bin: lando.config.containerdBin || path.join(binDir, "containerd"), | ||
| version: "2.0.4", | ||
| tarballEntry: "bin/containerd", | ||
| }, | ||
| { | ||
| name: "buildkitd", | ||
| id: "setup-buildkitd", | ||
| bin: lando.config.buildkitdBin || path.join(binDir, "buildkitd"), | ||
| version: "0.18.2", | ||
| tarballEntry: "bin/buildkitd", | ||
| dependsOn: ["setup-containerd"], | ||
| }, | ||
| { | ||
| name: "nerdctl", | ||
| id: "setup-nerdctl", | ||
| bin: lando.config.nerdctlBin || path.join(binDir, "nerdctl"), | ||
| version: "2.0.5", | ||
| tarballEntry: "bin/nerdctl", | ||
| dependsOn: ["setup-buildkitd"], | ||
| }, | ||
| ]; | ||
|
|
||
| for (const binary of binaries) { | ||
| const url = getUrl(binary.name === "buildkitd" ? "buildkit" : binary.name, {version: binary.version}); | ||
|
|
||
| const task = { | ||
| title: `Installing ${binary.name}`, | ||
| id: binary.id, | ||
| description: `@lando/${binary.name} (containerd engine)`, | ||
| version: `${binary.name} v${binary.version}`, | ||
| hasRun: async () => fs.existsSync(binary.bin), | ||
| canRun: async () => { | ||
| if (engine === "auto") { | ||
| // In auto mode, skip containerd setup if Docker is already working | ||
| try { | ||
| if (lando.engine && lando.engine.dockerInstalled) return false; | ||
| } catch {} | ||
| } | ||
| await axios.head(url); | ||
| return true; | ||
| }, | ||
| task: async (ctx, task) => { | ||
| // Download the tarball | ||
| const tmpDir = path.join(os.tmpdir(), `lando-${binary.name}-${Date.now()}`); | ||
| fs.mkdirSync(tmpDir, {recursive: true}); | ||
| fs.mkdirSync(binDir, {recursive: true}); | ||
|
|
||
| await new Promise((resolve, reject) => { | ||
| const download = require("../utils/download-x")(url, { | ||
| debug, | ||
| dest: path.join(tmpDir, `${binary.name}.tar.gz`), | ||
| }); | ||
| download.on("done", resolve); | ||
| download.on("error", reject); | ||
| download.on("progress", progress => { | ||
| task.title = `Downloading ${binary.name} ${color.dim(`[${progress.percentage}%]`)}`; | ||
| }); | ||
| }); | ||
|
|
||
| // Extract the specific binary from the tarball | ||
| task.title = `Extracting ${binary.name}...`; | ||
| const {execSync} = require("child_process"); | ||
| execSync( | ||
| `tar -xzf "${path.join(tmpDir, binary.name + ".tar.gz")}" -C "${tmpDir}" "${binary.tarballEntry}"`, | ||
| {stdio: "pipe"}, | ||
| ); | ||
|
|
||
| // Move to bin dir | ||
| const extracted = path.join(tmpDir, binary.tarballEntry); | ||
| const dest = binary.bin; | ||
| fs.copyFileSync(extracted, dest); | ||
| require("../utils/make-executable")([dest]); | ||
|
|
||
| // Cleanup temp | ||
| fs.rmSync(tmpDir, {recursive: true, force: true}); | ||
|
|
||
| task.title = `Installed ${binary.name} to ${dest}`; | ||
| }, | ||
| }; | ||
|
|
||
| if (binary.dependsOn) task.dependsOn = binary.dependsOn; | ||
| options.tasks.push(task); | ||
| } | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unawaited promise in containerd compatibility check hook
Medium Severity
lando.engine.getCompatibility().then(...)is called withoutawait, so the promise is fire-and-forget. The cache andlando.versionsupdates may not complete before downstream code reads them, leading to stale or missing version/compatibility data. The equivalent Docker hook (lando-get-compat.js) likely has the same pattern, but this is a new introduction that could cause race conditions with containerd-specific version checks.