From 05e7d8c5294358c36475d97caa2b6d56ab510fe9 Mon Sep 17 00:00:00 2001 From: Thomas Camlong Date: Wed, 1 Jul 2026 22:30:44 +0200 Subject: [PATCH] feat(onboarding): add Docker label discovery during init Port label-based container discovery from config-portability and wire it into the onboarding integrations step. Labeled containers take priority over image matching, sync into category sections, and preserve board layout when placing integration widgets. --- .../docs/integrations/docker-labels/index.mdx | 54 ++ .../docs/integrations/docker-labels/index.ts | 8 + apps/docs/docs/integrations/docker/index.mdx | 6 +- .../_steps/integrations/init-integrations.tsx | 48 +- .../api/src/router/onboard/onboard-router.ts | 207 ++++++-- .../mysql/0042_add_docker_app_source.sql | 17 + .../db/migrations/mysql/meta/_journal.json | 7 + .../postgresql/0008_add_docker_app_source.sql | 17 + .../migrations/postgresql/meta/_journal.json | 7 + .../sqlite/0040_add_docker_app_source.sql | 17 + .../db/migrations/sqlite/meta/_journal.json | 7 + packages/db/schema/index.ts | 1 + packages/db/schema/mysql.ts | 46 ++ packages/db/schema/postgresql.ts | 46 ++ packages/db/schema/sqlite.ts | 46 ++ packages/docker/package.json | 8 +- .../__tests__/parse-container-labels.spec.ts | 469 ++++++++++++++++++ packages/docker/src/discovery/index.ts | 8 + .../discovery/list-discovered-containers.ts | 45 ++ .../src/discovery/parse-container-labels.ts | 110 ++++ .../src/discovery/sync-discovered-services.ts | 454 +++++++++++++++++ packages/docker/src/discovery/types.ts | 16 + packages/docker/src/index.ts | 1 + packages/docker/src/labels.ts | 19 +- packages/server-settings/src/index.ts | 9 + pnpm-lock.yaml | 15 +- 26 files changed, 1630 insertions(+), 58 deletions(-) create mode 100644 apps/docs/docs/integrations/docker-labels/index.mdx create mode 100644 apps/docs/docs/integrations/docker-labels/index.ts create mode 100644 packages/db/migrations/mysql/0042_add_docker_app_source.sql create mode 100644 packages/db/migrations/postgresql/0008_add_docker_app_source.sql create mode 100644 packages/db/migrations/sqlite/0040_add_docker_app_source.sql create mode 100644 packages/docker/src/discovery/__tests__/parse-container-labels.spec.ts create mode 100644 packages/docker/src/discovery/index.ts create mode 100644 packages/docker/src/discovery/list-discovered-containers.ts create mode 100644 packages/docker/src/discovery/parse-container-labels.ts create mode 100644 packages/docker/src/discovery/sync-discovered-services.ts create mode 100644 packages/docker/src/discovery/types.ts diff --git a/apps/docs/docs/integrations/docker-labels/index.mdx b/apps/docs/docs/integrations/docker-labels/index.mdx new file mode 100644 index 000000000..e478381a2 --- /dev/null +++ b/apps/docs/docs/integrations/docker-labels/index.mdx @@ -0,0 +1,54 @@ +--- +title: 'Docker labels' +description: 'Configure Homarr apps automatically using Docker container labels during onboarding.' +hide_title: true +--- + +# Docker label discovery + +Homarr can read `homarr.*` labels on your containers to pre-populate apps during onboarding. Homepage-style `homepage.*` labels are supported as a fallback when no `homarr.name` label is set. + +## Required labels + +| Label | Description | +| --- | --- | +| `homarr.name` | Display name for the app | +| `homarr.group` | Category section name on the board | +| `homarr.href` | URL opened when clicking the app | + +## Optional labels + +| Label | Description | +| --- | --- | +| `homarr.icon` | Icon URL | +| `homarr.description` | App description | +| `homarr.ping` | Ping URL for status checks | +| `homarr.id` | Stable external ID (defaults to container ID) | +| `homarr.board` | Target board name | +| `homarr.integration` | Integration kind (e.g. `sonarr`) for onboarding integration setup | +| `homarr.widget` | Widget kind hint (parsed, not auto-placed yet) | +| `homarr.hide` | Hide container from discovery | + +## Homepage fallback + +When `homarr.name` is absent, Homarr reads `homepage.name`, `homepage.group`, and `homepage.href` if homepage label reading is enabled (default: on). + +## Example + +```yaml +services: + sonarr: + image: lscr.io/linuxserver/sonarr:latest + labels: + homarr.name: "Sonarr" + homarr.group: "Media" + homarr.href: "http://sonarr:8989" + homarr.icon: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sonarr.svg" + homarr.integration: "sonarr" +``` + +## Onboarding behavior + +During the integrations step, Homarr scans labeled containers first. Labeled services appear with their configured name, group, and URL. Unlabeled running containers still use image-based discovery as a fallback. + +When you finish onboarding, selected label-based apps are synced into category sections on your board. Integration widgets are placed separately in the default section. diff --git a/apps/docs/docs/integrations/docker-labels/index.ts b/apps/docs/docs/integrations/docker-labels/index.ts new file mode 100644 index 000000000..1b9d18cab --- /dev/null +++ b/apps/docs/docs/integrations/docker-labels/index.ts @@ -0,0 +1,8 @@ +import { IntegrationDefinition } from "@site/src/types"; + +export const dockerLabelsIntegration: IntegrationDefinition = { + name: "Docker labels", + description: "Configure Homarr apps automatically using Docker container labels.", + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/docker.svg", + path: "../../integrations/docker-labels", +}; diff --git a/apps/docs/docs/integrations/docker/index.mdx b/apps/docs/docs/integrations/docker/index.mdx index e48c46e9b..732708f9d 100644 --- a/apps/docs/docs/integrations/docker/index.mdx +++ b/apps/docs/docs/integrations/docker/index.mdx @@ -93,9 +93,11 @@ They are then available in the app list and can be used as normally added apps. ### Configuration -Currently Homarr supports one label for configuration of the containers: +Homarr supports Docker labels for automatic app discovery during onboarding. See [Docker labels](./docker-labels/index.mdx) for the full reference. -- **homarr.hide:** If set to any value, the container will be hidden from the Docker Containers widget and tools page. +Currently Homarr supports one label for hiding containers: + +- **homarr.hide:** If set to any value, the container will be hidden from the Docker Containers widget, tools page, and label discovery. ### Security Mounting docker sockets can be risky, as they permit full control over your docker service. diff --git a/apps/nextjs/src/app/[locale]/init/_steps/integrations/init-integrations.tsx b/apps/nextjs/src/app/[locale]/init/_steps/integrations/init-integrations.tsx index 71f9345f3..ca0a1608b 100644 --- a/apps/nextjs/src/app/[locale]/init/_steps/integrations/init-integrations.tsx +++ b/apps/nextjs/src/app/[locale]/init/_steps/integrations/init-integrations.tsx @@ -50,7 +50,7 @@ interface IntegrationDraft { initialUrl: string; initialName?: string; dockerPort?: number | null; - source: "manual" | "docker"; + source: "manual" | "docker" | "label"; } export const InitIntegrations = () => { @@ -65,6 +65,7 @@ export const InitIntegrations = () => { const { mutateAsync: setupIntegrations } = clientApi.onboard.setupIntegrations.useMutation(); const { mutateAsync: createApps } = clientApi.onboard.createAppsFromDiscovery.useMutation(); + const { mutateAsync: syncLabelApps } = clientApi.onboard.syncDockerLabelApps.useMutation(); const { data: dockerData, isPending: isDockerLoading } = clientApi.onboard.discoverDockerServices.useQuery( undefined, @@ -98,7 +99,7 @@ export const InitIntegrations = () => { initialUrl: item.suggestedUrl, initialName: item.containerName, dockerPort: item.publishedPort, - source: "docker", + source: item.source, }); } newKinds.add(item.kind); @@ -132,7 +133,8 @@ export const InitIntegrations = () => { const newDrafts = new Map(drafts); for (const kind of selectedKinds) { const existing = newDrafts.get(kind); - if (baseHost) { + const useUrlTemplate = baseHost && existing?.source !== "label"; + if (useUrlTemplate) { newDrafts.set(kind, { kind, initialUrl: buildIntegrationUrl(kind, baseHost, urlMode, existing?.dockerPort ?? undefined), @@ -152,10 +154,25 @@ export const InitIntegrations = () => { const finishSetupAsync = async () => { setPhase("done"); try { - const appsToCreate = dockerApps.filter((app) => selectedAppIds.has(app.containerId)); - if (appsToCreate.length > 0) { + const selectedApps = dockerApps.filter((app) => selectedAppIds.has(app.containerId)); + const labelApps = selectedApps.filter((app) => app.source === "label"); + const imageApps = selectedApps.filter((app) => app.source !== "label"); + + if (labelApps.length > 0) { + const labelAppsWithHost = labelApps.filter((app): app is (typeof app & { host: string }) => Boolean(app.host)); + if (labelAppsWithHost.length > 0) { + await syncLabelApps( + labelAppsWithHost.map((app) => ({ + containerId: app.containerId, + host: app.host, + })), + ); + } + } + + if (imageApps.length > 0) { await createApps( - appsToCreate.map((app) => { + imageApps.map((app) => { const href = baseHost ? buildAppUrl(app.containerName, baseHost, urlMode, app.publishedPort ?? undefined) : app.suggestedUrl; @@ -167,6 +184,7 @@ export const InitIntegrations = () => { }), ); } + await setupIntegrations(); router.refresh(); } catch { @@ -336,9 +354,16 @@ export const InitIntegrations = () => { > - - {app.containerName} - + + + {app.containerName} + + {app.group && ( + + {app.group} + + )} + {isSelected && } @@ -360,7 +385,7 @@ export const InitIntegrations = () => {