[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Build a Container Orchestration Dashboard

Build a Container Orchestration Dashboard

Build a real-time dashboard that visualizes Docker container health, resource usage, logs, and deployments — giving teams instant infrastructure visibility without kubectl.

Development#typescript#tsconfig#type-checking#type-safety#javascript-migration
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 96/100·All SAFE
>

typescript

v1.0.0

Sets up and repairs TypeScript projects: picks tsconfig.json settings for a Node.js service, an npm library or a bundled web app, runs type-checking in CI, and replaces `any` and unsafe casts with types the compiler can verify. Use when someone asks to "set up TypeScript", "fix this tsconfig", "fix a TypeScript error", "upgrade to TypeScript 6 or 7", "migrate JavaScript to TypeScript", "publish types for a package", "run .ts files in Node" or "make this type-safe". Covers the changed defaults in TypeScript 6.0 and 7.0, module and moduleResolution, strict mode, project references, declaration output, narrowing, satisfies and common compiler errors.

93/100 quality
7.30× impact
SAFE· stale score
View skill
>

nextjs

v1.1.0

Assists with building production-grade React applications using Next.js 16. Use when working with the App Router, Server Components, Server Actions, the proxy file (formerly middleware), Cache Components, or deploying to Vercel or self-hosted environments. Trigger words: nextjs, next.js, app router, server components, server actions, use cache, proxy.ts, react framework, ssr, isr.

100/100 quality
5.29× impact
SAFE· stale score
View skill
>

redis

v1.1.0

Redis is an in-memory data store used as a cache, session store, message broker and counter service. Use when adding a cache layer, storing sessions, rate limiting an API, running pub/sub or Streams with consumer groups, taking a distributed lock, building a leaderboard, or configuring Redis for production (persistence, maxmemory, eviction).

93/100 quality
1.79× impact
SAFE· stale score
View skill
>

docker-helper

v1.1.0

Build, debug, and optimize Docker configurations. Use when a user asks to create a Dockerfile, fix Docker build errors, optimize image size, write docker-compose files, debug container issues, set up multi-stage builds, or troubleshoot networking between containers. Covers Dockerfile best practices and compose orchestration.

100/100 quality
1.44× impact
SAFE· stale score
View skill
>

tailwindcss

v1.1.0

Tailwind CSS is a utility-first CSS framework: you style pages with small classes in the markup and ship only the CSS you use. Covers Tailwind v4 (CSS-first config with @theme, @import "tailwindcss", Vite and PostCSS plugins), responsive layouts, dark mode, custom animations, and the v3 to v4 renames. Use when someone asks to "style with Tailwind", "set up Tailwind v4", "upgrade Tailwind to v4", "add dark mode", or "fix Tailwind classes that stopped working".

93/100 quality
2.28× impact
SAFE· stale score
View skill
$

The Problem

Leo manages infrastructure at a 25-person startup running 40+ Docker containers across 3 servers. When something breaks at 2 AM, the on-call engineer SSHes into each server, runs docker ps, docker logs, and docker stats — a 15-minute process just to find which container is unhealthy. There's no centralized view of container health, no alerting on resource spikes, and deployments are manual SSH + docker-compose commands. A dashboard would give the team instant visibility and reduce incident response time from 15 minutes to 30 seconds.

Step 1: Build the Container Metrics Collector

An agent runs on each Docker host, collecting container stats via the Docker API and pushing them to Redis for real-time aggregation.

typescript
// src/agent/collector.ts — Docker metrics collector agent
import Docker from "dockerode";
import { Redis } from "ioredis";

const docker = new Docker({ socketPath: "/var/run/docker.sock" });
const redis = new Redis(process.env.REDIS_URL!);
const HOST_ID = process.env.HOST_ID || "host-1";

interface ContainerMetrics {
  containerId: string;
  name: string;
  image: string;
  status: string;
  state: string;
  health: string | null;
  cpuPercent: number;
  memoryUsageMB: number;
  memoryLimitMB: number;
  memoryPercent: number;
  networkRxMB: number;
  networkTxMB: number;
  blockReadMB: number;
  blockWriteMB: number;
  restartCount: number;
  uptime: string;
  ports: Array<{ host: number; container: number; protocol: string }>;
  labels: Record<string, string>;
  hostId: string;
  collectedAt: number;
}

export async function collectMetrics(): Promise<ContainerMetrics[]> {
  const containers = await docker.listContainers({ all: true });
  const metrics: ContainerMetrics[] = [];

  for (const containerInfo of containers) {
    try {
      const container = docker.getContainer(containerInfo.Id);
      const inspect = await container.inspect();

      let cpuPercent = 0;
      let memUsage = 0;
      let memLimit = 0;
      let netRx = 0;
      let netTx = 0;

      // Get live stats (one-shot, don't stream)
      if (containerInfo.State === "running") {
        const stats = await container.stats({ stream: false });

        // CPU calculation
        const cpuDelta = stats.cpu_stats.cpu_usage.total_usage - stats.precpu_stats.cpu_usage.total_usage;
        const systemDelta = stats.cpu_stats.system_cpu_usage - stats.precpu_stats.system_cpu_usage;
        const numCpus = stats.cpu_stats.online_cpus || 1;
        cpuPercent = systemDelta > 0 ? (cpuDelta / systemDelta) * numCpus * 100 : 0;

        // Memory
        memUsage = stats.memory_stats.usage - (stats.memory_stats.stats?.cache || 0);
        memLimit = stats.memory_stats.limit;

        // Network
        if (stats.networks) {
          for (const net of Object.values(stats.networks) as any[]) {
            netRx += net.rx_bytes;
            netTx += net.tx_bytes;
          }
        }
      }

      const metric: ContainerMetrics = {
        containerId: containerInfo.Id.slice(0, 12),
        name: containerInfo.Names[0]?.replace("/", "") || "unknown",
        image: containerInfo.Image,
        status: containerInfo.Status,
        state: containerInfo.State,
        health: inspect.State.Health?.Status || null,
        cpuPercent: Math.round(cpuPercent * 100) / 100,
        memoryUsageMB: Math.round(memUsage / 1048576),
        memoryLimitMB: Math.round(memLimit / 1048576),
        memoryPercent: memLimit > 0 ? Math.round((memUsage / memLimit) * 10000) / 100 : 0,
        networkRxMB: Math.round(netRx / 1048576 * 100) / 100,
        networkTxMB: Math.round(netTx / 1048576 * 100) / 100,
        blockReadMB: 0,
        blockWriteMB: 0,
        restartCount: inspect.RestartCount,
        uptime: containerInfo.Status,
        ports: (containerInfo.Ports || []).map((p) => ({
          host: p.PublicPort,
          container: p.PrivatePort,
          protocol: p.Type,
        })),
        labels: containerInfo.Labels,
        hostId: HOST_ID,
        collectedAt: Date.now(),
      };

      metrics.push(metric);
    } catch {
      // Container may have stopped between list and inspect
    }
  }

  return metrics;
}

// Push metrics to Redis for the dashboard to consume
export async function pushMetrics(): Promise<void> {
  const metrics = await collectMetrics();

  const pipeline = redis.pipeline();

  for (const metric of metrics) {
    const key = `container:${HOST_ID}:${metric.containerId}`;
    pipeline.setex(key, 30, JSON.stringify(metric)); // expires if agent stops

    // Store in time series for historical charts
    pipeline.zadd(
      `metrics:${metric.containerId}:cpu`,
      metric.collectedAt,
      JSON.stringify({ t: metric.collectedAt, v: metric.cpuPercent })
    );
    pipeline.zadd(
      `metrics:${metric.containerId}:memory`,
      metric.collectedAt,
      JSON.stringify({ t: metric.collectedAt, v: metric.memoryPercent })
    );

    // Trim to last hour of data
    const oneHourAgo = Date.now() - 3600000;
    pipeline.zremrangebyscore(`metrics:${metric.containerId}:cpu`, 0, oneHourAgo);
    pipeline.zremrangebyscore(`metrics:${metric.containerId}:memory`, 0, oneHourAgo);
  }

  // Store host-level summary
  pipeline.setex(`host:${HOST_ID}`, 30, JSON.stringify({
    hostId: HOST_ID,
    containerCount: metrics.length,
    runningCount: metrics.filter((m) => m.state === "running").length,
    totalCpuPercent: metrics.reduce((s, m) => s + m.cpuPercent, 0),
    totalMemoryMB: metrics.reduce((s, m) => s + m.memoryUsageMB, 0),
    collectedAt: Date.now(),
  }));

  await pipeline.exec();

  // Publish for real-time WebSocket updates
  await redis.publish("container:updates", JSON.stringify({
    hostId: HOST_ID,
    containers: metrics,
    timestamp: Date.now(),
  }));
}

// Run collection every 5 seconds
setInterval(pushMetrics, 5000);
pushMetrics(); // initial collection

Step 2: Build the Dashboard API

The API aggregates metrics from all hosts and serves them to the dashboard frontend.

typescript
// src/api/routes.ts — Dashboard API endpoints
import { Hono } from "hono";
import { Redis } from "ioredis";

const redis = new Redis(process.env.REDIS_URL!);
const app = new Hono();

// Get all containers across all hosts
app.get("/containers", async (c) => {
  const keys = await redis.keys("container:*:*");
  const pipeline = redis.pipeline();
  keys.forEach((key) => pipeline.get(key));
  const results = await pipeline.exec();

  const containers = results!
    .filter(([err, val]) => !err && val)
    .map(([, val]) => JSON.parse(val as string))
    .sort((a, b) => a.name.localeCompare(b.name));

  return c.json({ containers, total: containers.length });
});

// Get all hosts summary
app.get("/hosts", async (c) => {
  const keys = await redis.keys("host:*");
  const pipeline = redis.pipeline();
  keys.forEach((key) => pipeline.get(key));
  const results = await pipeline.exec();

  const hosts = results!
    .filter(([err, val]) => !err && val)
    .map(([, val]) => JSON.parse(val as string));

  return c.json({ hosts });
});

// Get container metrics history (last hour)
app.get("/containers/:id/metrics", async (c) => {
  const { id } = c.req.param();
  const metric = c.req.query("metric") || "cpu"; // cpu or memory

  const data = await redis.zrangebyscore(
    `metrics:${id}:${metric}`,
    Date.now() - 3600000,
    "+inf"
  );

  const points = data.map((d) => JSON.parse(d));
  return c.json({ metric, points });
});

// Get container logs (last 100 lines)
app.get("/containers/:id/logs", async (c) => {
  const { id } = c.req.param();
  const lines = Number(c.req.query("lines") || 100);

  // Find which host has this container
  const keys = await redis.keys(`container:*:${id}`);
  if (keys.length === 0) return c.json({ error: "Container not found" }, 404);

  const containerData = JSON.parse((await redis.get(keys[0]))!);

  // Logs are fetched on-demand via the agent
  await redis.publish("log:request", JSON.stringify({
    containerId: id,
    hostId: containerData.hostId,
    lines,
  }));

  // Wait for log response (agent publishes to log:response:{id})
  return new Promise((resolve) => {
    const sub = new Redis(process.env.REDIS_URL!);
    const timeout = setTimeout(() => {
      sub.disconnect();
      resolve(c.json({ error: "Log fetch timeout" }, 504));
    }, 5000);

    sub.subscribe(`log:response:${id}`);
    sub.on("message", (channel, message) => {
      clearTimeout(timeout);
      sub.disconnect();
      resolve(c.json(JSON.parse(message)));
    });
  });
});

// Container actions: restart, stop, start
app.post("/containers/:id/:action", async (c) => {
  const { id, action } = c.req.param();
  if (!["restart", "stop", "start"].includes(action)) {
    return c.json({ error: "Invalid action" }, 400);
  }

  const keys = await redis.keys(`container:*:${id}`);
  if (keys.length === 0) return c.json({ error: "Container not found" }, 404);

  const containerData = JSON.parse((await redis.get(keys[0]))!);

  await redis.publish("container:action", JSON.stringify({
    containerId: id,
    hostId: containerData.hostId,
    action,
    requestedBy: c.get("userId") || "dashboard",
  }));

  return c.json({ success: true, action, containerId: id });
});

export default app;

Step 3: Build the Dashboard UI

The frontend displays a real-time grid of containers with health indicators, resource gauges, and one-click actions.

typescript
// src/components/ContainerGrid.tsx — Real-time container dashboard
import { useState, useEffect } from "react";

interface Container {
  containerId: string;
  name: string;
  image: string;
  state: string;
  health: string | null;
  cpuPercent: number;
  memoryPercent: number;
  memoryUsageMB: number;
  memoryLimitMB: number;
  restartCount: number;
  hostId: string;
  uptime: string;
}

export function ContainerGrid() {
  const [containers, setContainers] = useState<Container[]>([]);
  const [filter, setFilter] = useState<string>("all");

  useEffect(() => {
    const fetchContainers = async () => {
      const res = await fetch("/api/containers");
      const data = await res.json();
      setContainers(data.containers);
    };

    fetchContainers();
    const interval = setInterval(fetchContainers, 5000);

    return () => clearInterval(interval);
  }, []);

  const filtered = filter === "all"
    ? containers
    : containers.filter((c) => c.state === filter);

  const running = containers.filter((c) => c.state === "running").length;
  const stopped = containers.filter((c) => c.state !== "running").length;
  const unhealthy = containers.filter((c) => c.health === "unhealthy").length;

  return (
    <div className="p-6">
      {/* Summary bar */}
      <div className="grid grid-cols-4 gap-4 mb-6">
        <StatCard label="Total" value={containers.length} color="blue" />
        <StatCard label="Running" value={running} color="green" />
        <StatCard label="Stopped" value={stopped} color="gray" />
        <StatCard label="Unhealthy" value={unhealthy} color="red" />
      </div>

      {/* Filter tabs */}
      <div className="flex gap-2 mb-4">
        {["all", "running", "exited", "restarting"].map((f) => (
          <button key={f} onClick={() => setFilter(f)}
            className={`px-3 py-1 rounded text-sm ${filter === f ? "bg-blue-600 text-white" : "bg-gray-100"}`}>
            {f.charAt(0).toUpperCase() + f.slice(1)}
          </button>
        ))}
      </div>

      {/* Container cards */}
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
        {filtered.map((container) => (
          <ContainerCard key={container.containerId} container={container} />
        ))}
      </div>
    </div>
  );
}

function ContainerCard({ container }: { container: Container }) {
  const stateColors: Record<string, string> = {
    running: "border-green-400 bg-green-50",
    exited: "border-red-400 bg-red-50",
    restarting: "border-yellow-400 bg-yellow-50",
  };

  const handleAction = async (action: string) => {
    await fetch(`/api/containers/${container.containerId}/${action}`, { method: "POST" });
  };

  return (
    <div className={`border-l-4 rounded-lg p-4 bg-white shadow-sm ${stateColors[container.state] || "border-gray-400"}`}>
      <div className="flex justify-between items-start mb-3">
        <div>
          <h3 className="font-semibold text-gray-900">{container.name}</h3>
          <p className="text-xs text-gray-500 font-mono">{container.image.split(":")[0]}</p>
        </div>
        <div className="flex items-center gap-1">
          {container.health === "unhealthy" && <span className="text-red-500 text-xs">⚠️</span>}
          <span className={`text-xs px-2 py-0.5 rounded-full ${
            container.state === "running" ? "bg-green-100 text-green-700" : "bg-red-100 text-red-700"
          }`}>{container.state}</span>
        </div>
      </div>

      {/* Resource gauges */}
      <div className="space-y-2 mb-3">
        <ResourceBar label="CPU" value={container.cpuPercent} max={100} unit="%" warn={80} />
        <ResourceBar label="MEM" value={container.memoryPercent} max={100}
          unit={`${container.memoryUsageMB}/${container.memoryLimitMB}MB`} warn={85} />
      </div>

      <div className="flex justify-between items-center text-xs text-gray-500">
        <span>{container.uptime}</span>
        <span>{container.hostId}</span>
        {container.restartCount > 0 && (
          <span className="text-yellow-600">🔄 {container.restartCount}</span>
        )}
      </div>

      {/* Actions */}
      <div className="flex gap-2 mt-3 pt-3 border-t">
        {container.state === "running" ? (
          <>
            <ActionButton label="Restart" onClick={() => handleAction("restart")} />
            <ActionButton label="Stop" onClick={() => handleAction("stop")} danger />
            <ActionButton label="Logs" onClick={() => {/* open logs modal */}} />
          </>
        ) : (
          <ActionButton label="Start" onClick={() => handleAction("start")} />
        )}
      </div>
    </div>
  );
}

function ResourceBar({ label, value, max, unit, warn }: any) {
  const pct = Math.min((value / max) * 100, 100);
  const color = pct > warn ? "bg-red-500" : pct > warn * 0.7 ? "bg-yellow-500" : "bg-green-500";
  return (
    <div>
      <div className="flex justify-between text-xs text-gray-600 mb-1">
        <span>{label}</span>
        <span>{typeof unit === "string" && unit.includes("/") ? unit : `${value.toFixed(1)}${unit}`}</span>
      </div>
      <div className="h-2 bg-gray-200 rounded-full overflow-hidden">
        <div className={`h-full rounded-full transition-all ${color}`} style={{ width: `${pct}%` }} />
      </div>
    </div>
  );
}

function StatCard({ label, value, color }: any) {
  return (
    <div className={`bg-white rounded-lg p-4 border-l-4 border-${color}-500 shadow-sm`}>
      <div className="text-2xl font-bold">{value}</div>
      <div className="text-sm text-gray-500">{label}</div>
    </div>
  );
}

function ActionButton({ label, onClick, danger }: any) {
  return (
    <button onClick={onClick}
      className={`px-3 py-1 text-xs rounded ${danger ? "text-red-600 hover:bg-red-50" : "text-gray-600 hover:bg-gray-100"} border`}>
      {label}
    </button>
  );
}

Results

After deploying the container dashboard across 3 servers:

  • Incident identification time: from 15 minutes to 30 seconds — one glance at the dashboard shows which container is unhealthy, high on CPU, or restarting; no more SSH-ing into servers
  • Container restarts handled in 2 clicks — the dashboard's restart button replaces SSH + docker restart; junior engineers can respond to incidents without server access
  • Resource over-provisioning reduced by 35% — historical CPU/memory charts revealed that 12 containers were allocated 2-4x more memory than they actually used; right-sizing saved $400/month
  • Zero missed unhealthy containers — the health indicator and restart count badges make problems impossible to overlook; previously, an unhealthy container ran for 3 days before anyone noticed
  • Deployment visibility — teams see container image versions at a glance; "is the new version deployed?" no longer requires SSH

Related use cases