diff --git a/codersdk/health.go b/codersdk/health.go
index a53ca73192..a54b65762e 100644
--- a/codersdk/health.go
+++ b/codersdk/health.go
@@ -26,6 +26,7 @@ var HealthSections = []HealthSection{
HealthSectionWebsocket,
HealthSectionDatabase,
HealthSectionWorkspaceProxy,
+ HealthSectionProvisionerDaemons,
}
type HealthSettings struct {
diff --git a/docs/admin/healthcheck.md b/docs/admin/healthcheck.md
index a85d6f50ec..62a7de6197 100644
--- a/docs/admin/healthcheck.md
+++ b/docs/admin/healthcheck.md
@@ -267,6 +267,54 @@ _One or more Workspace Proxies Unhealthy_
**Solution:** Ensure that Coder can establish a connection to the configured
workspace proxies.
+### EPD01
+
+_No Provisioner Daemons Available_
+
+**Problem:** No provisioner daemons are registered with Coder. No workspaces can
+be built until there is at least one provisioner daemon running.
+
+**Solution:**
+
+If you are using
+[External Provisioner Daemons](./provisioners.md#external-provisioners), ensure
+that they are able to successfully connect to Coder. Otherwise, ensure
+[`--provisioner-daemons`](../cli/server.md#provisioner-daemons) is set to a
+value greater than 0.
+
+> Note: This may be a transient issue if you are currently in the process of
+> updating your deployment.
+
+### EPD02
+
+_Provisioner Daemon Version Mismatch_
+
+**Problem:** One or more provisioner daemons are more than one major or minor
+version out of date with the main deployment. It is important that provisioner
+daemons are updated at the same time as the main deployment to minimize the risk
+of API incompatibility.
+
+**Solution:** Update the provisioner daemon to match the currently running
+version of Coder.
+
+> Note: This may be a transient issue if you are currently in the process of
+> updating your deployment.
+
+### EPD03
+
+_Provisioner Daemon API Version Mismatch_
+
+**Problem:** One or more provisioner daemons are using APIs that are marked as
+deprecated. These deprecated APIs may be removed in a future release of Coder,
+at which point the affected provisioner daemons will no longer be able to
+connect to Coder.
+
+**Solution:** Update the provisioner daemon to match the currently running
+version of Coder.
+
+> Note: This may be a transient issue if you are currently in the process of
+> updating your deployment.
+
## EUNKNOWN
_Unknown Error_
diff --git a/site/src/AppRouter.tsx b/site/src/AppRouter.tsx
index 5d19dd3b88..245e263586 100644
--- a/site/src/AppRouter.tsx
+++ b/site/src/AppRouter.tsx
@@ -234,6 +234,9 @@ const WebsocketPage = lazy(() => import("./pages/HealthPage/WebsocketPage"));
const WorkspaceProxyHealthPage = lazy(
() => import("./pages/HealthPage/WorkspaceProxyPage"),
);
+const ProvisionerDaemonsHealthPage = lazy(
+ () => import("./pages/HealthPage/ProvisionerDaemonsPage"),
+);
export const AppRouter: FC = () => {
return (
@@ -400,6 +403,10 @@ export const AppRouter: FC = () => {
path="workspace-proxy"
element={}
/>
+ }
+ />
{/* Using path="*"" means "match anything", so this route
acts like a catch-all for URLs that we don't have explicit
diff --git a/site/src/pages/HealthPage/HealthLayout.tsx b/site/src/pages/HealthPage/HealthLayout.tsx
index 0b80dc01b4..0aef42e457 100644
--- a/site/src/pages/HealthPage/HealthLayout.tsx
+++ b/site/src/pages/HealthPage/HealthLayout.tsx
@@ -34,6 +34,7 @@ export function HealthLayout() {
websocket: "Websocket",
database: "Database",
workspace_proxy: "Workspace Proxy",
+ provisioner_daemons: "Provisioner Daemons",
} as const;
const visibleSections = filterVisibleSections(sections);
diff --git a/site/src/pages/HealthPage/ProvisionerDaemonsPage.stories.tsx b/site/src/pages/HealthPage/ProvisionerDaemonsPage.stories.tsx
new file mode 100644
index 0000000000..7117aa8869
--- /dev/null
+++ b/site/src/pages/HealthPage/ProvisionerDaemonsPage.stories.tsx
@@ -0,0 +1,16 @@
+import { StoryObj, Meta } from "@storybook/react";
+import { ProvisionerDaemonsPage } from "./ProvisionerDaemonsPage";
+import { generateMeta } from "./storybook";
+
+const meta: Meta = {
+ title: "pages/Health/ProvisionerDaemons",
+ ...generateMeta({
+ path: "/health/provisioner-daemons",
+ element: ,
+ }),
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {};
diff --git a/site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx b/site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx
new file mode 100644
index 0000000000..6c797b9815
--- /dev/null
+++ b/site/src/pages/HealthPage/ProvisionerDaemonsPage.tsx
@@ -0,0 +1,166 @@
+import { Header, HeaderTitle, HealthyDot, Main, Pill } from "./Content";
+import { Helmet } from "react-helmet-async";
+import { pageTitle } from "utils/page";
+import { useTheme } from "@mui/material/styles";
+import { DismissWarningButton } from "./DismissWarningButton";
+import { Alert } from "components/Alert/Alert";
+import { HealthcheckReport } from "api/typesGenerated";
+import { createDayString } from "utils/createDayString";
+
+import { useOutletContext } from "react-router-dom";
+import Business from "@mui/icons-material/Business";
+import Person from "@mui/icons-material/Person";
+import SwapHoriz from "@mui/icons-material/SwapHoriz";
+import Tooltip from "@mui/material/Tooltip";
+import Sell from "@mui/icons-material/Sell";
+
+export const ProvisionerDaemonsPage = () => {
+ const healthStatus = useOutletContext();
+ const { provisioner_daemons: daemons } = healthStatus;
+ const theme = useTheme();
+ return (
+ <>
+
+ {pageTitle("Provisioner Daemons - Health")}
+
+
+
+
+
+ Provisioner Daemons
+
+
+
+
+
+ {daemons.warnings.map((warning) => {
+ return (
+
+ {warning.message}
+
+ );
+ })}
+
+ {daemons.items.map(({ provisioner_daemon: daemon, warnings }) => {
+ const daemonScope = daemon.tags["scope"] || "organization";
+ const iconScope =
+ daemonScope === "organization" ? : ;
+ const extraTags = Object.keys(daemon.tags)
+ .filter((key) => key !== "scope" && key !== "owner")
+ .reduce(
+ (acc, key) => {
+ acc[key] = daemon.tags[key];
+ return acc;
+ },
+ {} as Record,
+ );
+ const isWarning = warnings.length > 0;
+ return (
+