Skip to content

feat(nodes): add Docker API backend#394

Open
ArshSSandhu wants to merge 2 commits into
mieweb:mainfrom
ArshSSandhu:375-docker-node-api
Open

feat(nodes): add Docker API backend#394
ArshSSandhu wants to merge 2 commits into
mieweb:mainfrom
ArshSSandhu:375-docker-node-api

Conversation

@ArshSSandhu

Copy link
Copy Markdown
Collaborator

Summary

Closes #375.

This PR adds support for Docker-backed nodes by introducing a new DockerApi implementation that follows the existing Node API contract used by ProxmoxApi and DummyApi.

Changes include:

  • Added DockerApi using Docker Engine's native HTTP API through axios
  • Added support for Docker host formats:
    • unix:///var/run/docker.sock
    • tcp://host:2375
    • http://...
    • https://...
  • Updated Node.api() to dispatch to DockerApi when nodeType === "docker"
  • Added provider-neutral API access checks through node.hasApiAccess()
  • Updated container provisioning node selection to use Node.provisionableWhere(...)
  • Added nodeType support to node API serialization, create, and update routes
  • Updated the node form UI to support Proxmox, Docker, and dummy nodes
  • Allowed Docker host values such as unix:///var/run/docker.sock in the frontend form

Related Issue

Closes #375

Testing

Tested locally with the following checks:

  • Ran backend syntax checks successfully:

    node --check create-a-container/models/node.js
    node --check create-a-container/utils/docker-api.js
    node --check create-a-container/utils/container-status.js
    node --check create-a-container/routers/api/v1/containers.js
    node --check create-a-container/routers/api/v1/nodes.js
  • Ran frontend type-check successfully:

    npm run client:type-check
  • Ran whitespace checks successfully:

    git diff --check
    git diff --cached --check
  • Tested DockerApi directly against local Docker using:

    unix:///var/run/docker.sock
  • Verified direct Docker lifecycle flow:

    • Pulled nginx:latest
    • Created a Docker container
    • Started the container
    • Checked running status
    • Deleted the test container
  • Verified the UI can create a Docker node with:

    Name: local-docker
    Node type: Docker
    Docker host: unix:///var/run/docker.sock
  • Attempted full UI container creation with nginx:latest. The job selected the Docker node and Docker image path correctly, but failed with:

    connect ENOENT /var/run/docker.sock

    This appears to be a local runtime/socket-mount limitation because the Manager runtime does not currently have the host Docker socket mounted. The DockerApi itself was verified successfully through the direct lifecycle test above.

Screenshots

Docker node form

image

Docker node created

image

Breaking Changes

None known.

Notes

Full end-to-end Docker provisioning from the UI requires the Manager runtime to have access to the configured Docker host. For local socket usage, /var/run/docker.sock must be mounted or otherwise reachable from the process running Manager.

@runleveldev runleveldev self-requested a review July 8, 2026 15:32

@runleveldev runleveldev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The UI displays "Creds missing" for Docker nodes

Image

Running our default templates fail since they use SystemD. Similar to how Proxmox checks for "application containers" and make the nessecary adjustments, this needs to check for "system containers" and make the nessacary adjustments (such as setting privileged: true)

7a542e3b9e12   ghcr.io/mieweb/opensource-server/base:latest            "/sbin/init"             19 seconds ago   Exited (255) 17 seconds ago                hollow-urban-otter

Comment on lines 1 to +20
@@ -12,23 +12,24 @@ import {
Spinner,
Switch,
useToast,
} from '@mieweb/ui';
import { Server } from 'lucide-react';
import { api, ApiError } from '@/lib/api';
import { keys, queries } from '@/lib/queries';
import { FormPageLayout } from '@/components/FormPageLayout';
import type { Node } from '@/lib/types';
} from "@mieweb/ui";
import { Server } from "lucide-react";
import { api, ApiError } from "@/lib/api";
import { keys, queries } from "@/lib/queries";
import { FormPageLayout } from "@/components/FormPageLayout";
import type { Node } from "@/lib/types";

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'm assuming these changes were from an auto-formatter. Please avoid making formatting-only changes since we don't have a consistent formatting standard. I'm open to a PR to enforce formatting standards, but it should be a dedicated PR to avoid expanding feature diffs.

class DockerApi {
constructor(node = {}) {
this.node = node;
const dockerHost = node.apiUrl || process.env.DOCKER_HOST || 'unix:///var/run/docker.sock';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This should only be node.apiUrl. The fallbacks seem unreachable, since a provisionable node is guarded on the existence of node.apiUrl. If somehow this code is reached wthout the node having an apiUrl, this should throw due to violated preconditions.

const MANAGER_CONTAINER_LABEL = 'manager-os.container-id';

function parseDockerHost(host) {
const raw = host || process.env.DOCKER_HOST || 'unix:///var/run/docker.sock';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This fallback is the same as the one below and has the same issue: the fallbacks should be unreachable. I'll allow that the function should validate it's inputs (since this isn't typescript), but it should throw with precondition failed if typeof host !== 'string' (or similar).

Comment on lines +9 to +28
if (raw.startsWith('unix://')) {
const url = new URL(raw);
return {
baseURL: 'http://docker',
socketPath: decodeURIComponent(url.pathname),
};
}

if (raw.startsWith('tcp://')) {
const url = new URL(raw);
return {
baseURL: `http://${url.host}`,
};
}

if (raw.startsWith('http://') || raw.startsWith('https://')) {
return {
baseURL: raw.replace(/\/$/, ''),
};
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Similar to my earlier comments: this chunk of code allows for "bad" URLs and corrects them, such as by stripping path components. Rather than adjusting that here, the API server should validate that the URL is valid before it's ever saved into the database throwing a 4xx error if the user provided an invalid URL. The same validation code could be used here as a safetynet i.e.: if (!isValid(host)) throw.

Comment on lines +3 to +4
const MANAGER_NODE_LABEL = 'manager-os.node-id';
const MANAGER_CONTAINER_LABEL = 'manager-os.container-id';

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

We've been using the namespace of org.mieweb.opensource-server, these should follow that pattern, such as org.mieweb.opensource-server.node-id.


async storageContents() {
return [];
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is used by the create-container.js job to check for cached images. It might be sufficient to rely on pullOciImage below to handle caching since docker is better at it. If so, please note that in a comment near this function.

async createLxc(node, options = {}) {
if (!this.lastPulledImage) {
throw new Error('No Docker image has been pulled for this create operation');
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

This is an unnecessary check. Images might be cached for example.

Comment on lines +395 to +397
async waitForTask() {
return { status: 'stopped', exitstatus: 'OK' };
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this a no-op because all calls to the Docker API are synchronous? If so, please note that in a comment near this function.

Comment on lines +437 to +438
macAddress: await this.getLxcMacAddress(node, vmid),
ipv4Address: await this.getLxcIpAddress(node, vmid),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Doing this this way creates 2 round-trips to the API. Fine for local, could get painful if remote. You should be able to query the API once for both these pieces of information.

return !!(this.apiUrl && this.tokenId && this.secret);
}

static provisionableWhere(Sequelize) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can we import Sequelize into this file? Doesn't make sense to me to pass this as an argument.

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.

Docker-type Node API

2 participants