How to Build a Docker Extension from Scratch

Ajeet Singh Raina - Feb 20 '23 - - Dev Community

As a developer using Docker Desktop, you have access to a powerful tool that can help you build, share, and run your applications in a consistent and reliable way, across different environments and platforms.

Developing apps today is not limited to writing code. Tons of languages, frameworks, architectures and discontinuous interfaces between tools for each lifecycle stage create enormous complexity. Integration and User Experience is crucial to a frictionless developer workflow. For this reason, Docker created Docker Extensions to help developers reduce context switching and increase productivity. Using Extensions, developers can integrate seamlessly with external tools — and even create brand-new ones! Thanks to Docker Extensions SDK, now you have a quick start guide that gives you a jumpstart to building your own extensions for smooth, fast, and native integration directly into Docker Desktop.

In this blog, I will show you the power of Docker Extension in the true sense.

Prerequisite

Building a basic Docker Extensions

docker extension init demoapp
Enter fullscreen mode Exit fullscreen mode

Note: The docker extension init generates a React based extension. But you can still use it as a starting point for your own extension and use any other frontend framework, like Vue, Angular, Svelte, etc. or event stay with vanilla Javascript
.
The following command expects you to provide a name for your extension. It ask you title, description, vendor and any new Docker Hub repository name as shown below:

Results:

% docker extension init demoapp 
? Title: My First Demo App
? Description: This is my first demo application
? Vendor: Collabnix
? Image Repository where the extension will be pushed: ajeetraina/demoapp
Initializing empty Git repository in /Users/ajeetraina/feb/demoapp
Creating a Go backend service...
Initializing new go module...
Creating a React app...
Copying ui dir...
Renaming some files...
Installing npm packages, this may take a few minutes...
npm WARN deprecated sourcemap-codec@1.4.8: Please use @jridgewell/sourcemap-codec instead

added 475 packages, and audited 476 packages in 29s

41 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

🎉 Success! Created extension My First Demo App at /Users/ajeetraina/feb/demoapp

We suggest that you begin by typing:

  cd demoapp
  docker build -t ajeetraina/demoapp:latest .
  docker extension install ajeetraina/demoapp:latest

or use the targets defined in the Makefile. Then, open Docker Desktop and navigate to your extension.
Extension containers are hidden from the Docker Dashboard by default. You can change this in Settings > Extensions > Show Docker Extensions system containers.

To learn more about how to build your extension refer to the Extension SDK docs at https://docs.docker.com/desktop/extensions-sdk/.

To publish your extension in the Marketplace visit https://www.docker.com/products/extensions/submissions/.
To report issues and feedback visit https://github.com/docker/extensions-sdk/issues.

ajeetraina@Docker-Ajeet-Singh-Rainas-MacBook-Pro feb % 
....
Enter fullscreen mode Exit fullscreen mode

Understanding the Extension FileSystem Structure

When using the docker extension init, it creates a Dockerfile that already contains what is needed for a React extension.

├── Dockerfile # (1)
├── ui # (2)
│   ├── public # (3)
│   │   └── index.html
│   ├── src # (4)
│   │   ├── App.tsx
│   │   ├── index.tsx
│   ├── package.json
│   └── package-lock.lock
│   ├── tsconfig.json
├── docker.svg # (5)
└── metadata.json # (6)
Enter fullscreen mode Exit fullscreen mode
  1. Contains everything required to build the extension and run it in Docker Desktop.
  2. High-level folder containing your front-end app source code. Assets that aren’t compiled or dynamically generated are stored here. These can be static assets like logos or the robots.txt file.
  3. The src, or source folder contains all the React components, external CSS files, and dynamic assets that are brought into the component files.
  4. The icon that is displayed in the left-menu of the Docker Desktop Dashboard.
  5. A file that provides information about the extension such as the name, description, and version.

Understanding Dockerfile

cat Dockerfile 
FROM golang:1.19-alpine AS builder
ENV CGO_ENABLED=0
WORKDIR /backend
COPY backend/go.* .
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go mod download
COPY backend/. .
RUN --mount=type=cache,target=/go/pkg/mod \
    --mount=type=cache,target=/root/.cache/go-build \
    go build -trimpath -ldflags="-s -w" -o bin/service

FROM --platform=$BUILDPLATFORM node:18.12-alpine3.16 AS client-builder
WORKDIR /ui
# cache packages in layer
COPY ui/package.json /ui/package.json
COPY ui/package-lock.json /ui/package-lock.json
RUN --mount=type=cache,target=/usr/src/app/.npm \
    npm set cache /usr/src/app/.npm && \
    npm ci
# install
COPY ui /ui
RUN npm run build

FROM alpine
LABEL org.opencontainers.image.title="My First Demo App" \
    org.opencontainers.image.description="This is my first demo application" \
    org.opencontainers.image.vendor="Collabnix" \
    com.docker.desktop.extension.api.version="0.3.3" \
    com.docker.extension.screenshots="" \
    com.docker.extension.detailed-description="" \
    com.docker.extension.publisher-url="" \
    com.docker.extension.additional-urls="" \
    com.docker.extension.changelog=""

COPY --from=builder /backend/bin/service /
COPY docker-compose.yaml .
COPY metadata.json .
COPY docker.svg .
COPY --from=client-builder /ui/build ui
CMD /service -socket /run/guest-services/backend.sock
Enter fullscreen mode Exit fullscreen mode

Explanation:

Image45

This is a Dockerfile for building a multi-stage Docker image for a web application. Here's what it does:

  • The first stage starts with a Golang Alpine image and sets the environment variable CGO_ENABLED to 0.
  • The working directory is set to /backend and copies the go.mod and go.sum files to download and cache dependencies.
  • The go mod download command downloads and caches the dependencies based on the go.mod file. It uses the --mount flag to cache the dependencies in the target directories.
  • The current directory is copied to /backend.
  • The go build command is used to build the Go binary. The resulting binary is stored in the bin directory in the /backend directory.
  • The second stage starts with a Node.js Alpine image and sets the working directory to /ui.
  • The package.json and package-lock.json files are copied to cache the packages.
  • The npm ci command is used to install the packages based on the package.json and package-lock.json files. It uses the --mount flag to cache the packages in the .npm directory.
  • The ui directory is copied to the working directory.
  • The npm run build command is used to build the frontend application.
  • The final stage starts with an Alpine image and sets the metadata for the image using LABEL.
  • The Go binary is copied from the first stage to the root directory.
  • The docker-compose.yaml, metadata.json, docker.svg, and ui directory are copied to the root directory.
  • The CMD instruction specifies the command to run when the container starts.

When the Docker image is built, it results in a lightweight image with only the necessary files to run the application. The Go binary is used to serve the backend application, and the frontend application is served from the ui directory. The resulting Docker image can be deployed to a container platform to run the web application.

What is the difference between 1st stage and 2nd stage?

The Dockerfile provided has two stages: "builder" and "client-builder." Let's discuss the difference between these stages:

The 1st stage is "builder " stage and is responsible for building the backend service using Go.At the end of this stage, the compiled backend service binary (bin/service) is available in the /backend directory.

The 2dn stage is "client-builder" stage and this stage is responsible for building the frontend client using Node.js and React. At the end of this stage, the built React app is available in the /ui/build directory.

The main difference between these stages is the purpose they serve and the technologies used. The "builder" stage focuses on building the backend service using Go, while the "client-builder" stage focuses on building the frontend client using Node.js and React. By using separate stages, you can optimize the build process and reduce the size of the final Docker image by only including the necessary artifacts from each stage.

In summary, the "builder" stage compiles the backend service, and the "client-builder" stage builds the frontend client(React app). The final stage where we copy the backend binary and the React assets. Finally, the CMD instruction starts the backend service, using a socket to listen for requests from the React app.

Understanding metadata.json File

{
  "icon": "docker.svg",
  "vm": {
    "composefile": "docker-compose.yaml",
    "exposes": {
      "socket": "backend.sock"
    }
  },
  "ui": {
    "dashboard-tab": {
      "title": "My First Demo App",
      "src": "index.html",
      "root": "ui",
      "backend": {
        "socket": "backend.sock"
      }
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

This is a metadata.json file that defines metadata for a Docker desktop extension. Here's what it does:

  • The "icon" field specifies the path to the icon file for the extension.
  • The "vm" field specifies the configuration for the Docker VM that the extension will run on. The "composefile" field specifies the path to the Docker Compose file that defines the backend service. - - The "exposes" field specifies the socket file that the backend service will use to communicate with the extension.
  • The "ui" field specifies the configuration for the user interface of the extension. The "dashboard-tab" field specifies the title of the dashboard tab that will be displayed in the Docker Desktop UI. - The "src" field specifies the path to the HTML file that defines the content of the dashboard tab. The "root" field specifies the root directory for the UI files. The "backend" field specifies the socket file that the frontend will use to communicate with the backend service.

This metadata.json file is used by Docker Desktop to display information about the extension and to configure the extension's user interface.

Understanding the ui/src/App.tsx File

This is a basic React component that uses the Docker extension API to interact with the Docker Desktop backend. The component renders a page with some text and a button. When the button is clicked, it triggers a request to the backend, and the response is displayed in a textarea.

The createDockerDesktopClient function creates a client instance that can be used to interact with the Docker extension API. The useDockerDesktopClient hook returns this client instance for use within the component.

The fetchAndDisplayResponse function is called when the button is clicked. It uses the ddClient instance to send a GET request to the /hello endpoint of the Docker Desktop backend, and sets the response in the component state.

The component uses the Material-UI library to render the page and UI elements, including the button and the textarea for displaying the response.

Building the Extension

As instructed, change directory to demoapp/ and run the docker build command to build the Docker Extension.

cd demoapp
docker build -t ajeetraina/demoapp:latest .
Enter fullscreen mode Exit fullscreen mode

Results:

...
 => exporting to image                                                                                                 0.2s
 => => exporting layers                                                                                                0.2s
 => => exporting manifest sha256:9e940a7250881a62ba368469ce46c7278fcb4ad34aec8fd52a263cb0ba99c712                      0.0s
 => => exporting config sha256:4d6a0f1186add0253c842325936a1139abad7d14b96e3e692cb5fa1b16fb5b98                        0.0s
 => => naming to docker.io/ajeetraina/demoapp:latest                                                                   0.0s
 => => unpacking to docker.io/ajeetraina/demoapp:latest                                                                0.1s

Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them
Enter fullscreen mode Exit fullscreen mode

Running the Extension

% docker extension install ajeetraina/demoapp:latest
Extensions can install binaries, invoke commands and access files on your machine. 
Are you sure you want to continue? [y/N] y
Installing new extension "ajeetraina/demoapp:latest"
Installing service in Desktop VM...
Setting additional compose attributes
VM service started
Installing Desktop extension UI for tab "My First Demo App"...
Extension UI tab "My First Demo App" added.
Extension "My First Demo App" installed successfully
Enter fullscreen mode Exit fullscreen mode

Accessing the Extension

Open Docker Desktop > Dashboard > Extensions to view the Demo App Docker Extension.

This is a basic page rendered with MUI, using Docker's theme. Pressing the "Call Backend" button will trigger a request to the backend. Its response will appear in the text area.

Image2

Customising the UI

Let us try to build an extension that displays a simple list of containers using Docker Extensions SDK. This example shows how you can use the docker.cli.exec function to get the list of all the containers via the docker ps --all command and display the result in a table.

import React, { useEffect } from 'react';
import {
  Paper,
  Stack,
  Table,
  TableBody,
  TableCell,
  TableContainer,
  TableHead,
  TableRow,
  Typography
} from "@mui/material";
import { createDockerDesktopClient } from "@docker/extension-api-client";

//obtain docker destkop extension client
const ddClient = createDockerDesktopClient();

export function App() {
  const [containers, setContainers] = React.useState<any[]>([]);

  useEffect(() => {
    // List all containers
    ddClient.docker.cli.exec('ps', ['--all', '--format', '"{{json .}}"']).then((result) => {
      // result.parseJsonLines() parses the output of the command into an array of objects
      setContainers(result.parseJsonLines());
    });
  }, []);

  return (
    <Stack>
      <Typography data-testid="heading" variant="h3" role="title">
        Container list
      </Typography>
      <Typography
      data-testid="subheading"
      variant="body1"
      color="text.secondary"
      sx={{ mt: 2 }}
    >
      Simple list of containers using Docker Extensions SDK.
      </Typography>
      <TableContainer sx={{mt:2}}>
        <Table>
          <TableHead>
            <TableRow>
              <TableCell>Container id</TableCell>
              <TableCell>Image</TableCell>
              <TableCell>Command</TableCell>
              <TableCell>Created</TableCell>
              <TableCell>Status</TableCell>
            </TableRow>
          </TableHead>
          <TableBody>
            {containers.map((container) => (
              <TableRow
                key={container.ID}
                sx={{ '&:last-child td, &:last-child th': { border: 0 } }} >

                <TableCell>{container.ID}</TableCell>
                <TableCell>{container.Image}</TableCell>
                <TableCell>{container.Command}</TableCell>
                <TableCell>{container.CreatedAt}</TableCell>
                <TableCell>{container.Status}</TableCell>
              </TableRow>
            ))}
          </TableBody>
        </Table>
      </TableContainer>
    </Stack>
  );
}

Enter fullscreen mode Exit fullscreen mode

This code defines a React functional component called App. It uses the @mui/material library to render a table of information about Docker containers using the Docker Extensions SDK.

The code starts by importing various components from @mui/material, including Paper, Stack, Table, TableContainer, TableHead, TableBody, TableCell, and TableRow. It also imports React and createDockerDesktopClient from the @docker/extension-api-client library.

It defines a constant ddClient which obtains a client object for communicating with the Docker Desktop extension.

The main component code begins with a call to the useEffect hook to set up a side effect. In this case, the side effect is to list all the containers using the Docker CLI command docker ps --all --format "{{json .}}". The resulting JSON output is parsed into an array of objects and saved in the component state using the setContainers function. The useEffect hook only runs once, when the component mounts, since an empty dependency array is passed as the second argument.

The main return statement of the component is a Stack component that includes a Typography component with a heading and a subheading, followed by a TableContainer component. The TableContainer contains a Table component with a TableHead and a TableBody. The TableHead contains a row with table header cells, and the TableBody maps over the containers state array and creates a row for each container. The container information is displayed in table cells, with one row for each container in the containers array.

The component makes use of the data-testid attribute to provide test ids for the heading and subheading, which can be useful when writing automated tests for the component.

Re-building the Extension

docker extension update ajeetraina/demoapp:latest 
Extensions can install binaries, invoke commands and access files on your machine. 
Are you sure you want to continue? [y/N] y
updating "ajeetraina/demoapp" from "latest" to "latest"
Removing extension ajeetraina/demoapp:latest...
Removing extension containers...
Extension containers removed
VM service socket forwarding stopped
Extension UI tab My First Demo App removed
Extension "My First Demo App" removed
Installing new extension "ajeetraina/demoapp:latest"
Installing service in Desktop VM...
Setting additional compose attributes
VM service started
Installing Desktop extension UI for tab "My First Demo App"...
Extension UI tab "My First Demo App" added.
Extension "My First Demo App" installed successfully 
Enter fullscreen mode Exit fullscreen mode

Refresh the Docker Dashboard to see a new UI:

Image8

Few Things to Remember:

  • Extensions UI parts are isolated from each other and extension UI code is running in its own session for each extension. Extensions can’t access other extensions’ session data.
  • Extension UI code is rendered in a separate electron session and doesn’t have a node.js environment initialized, nor direct access to the electron APIs. This is to limit the possible unexpected side effects to the overall Docker Dashboard.
  • The extension UI code can’t perform privileged tasks, such as making changes to the system, or spawning sub-processes, except by using the SDK APIs provided with the extension framework. The Extension UI code can also perform interactions with Docker Desktop, such as navigating to various places in the Dashboard, only through the extension SDK APIs.

Using Material UI Grid

Open ui/src/App.tsx and modify the code to include Material UI Grid, button and Typography:

import { Grid, Button, Typography } from '@mui/material';
import { createDockerDesktopClient } from '@docker/extension-api-client';

export function App() {
  const ddClient = createDockerDesktopClient();

  function sayHello() {
    console.log('Hi Console');
    ddClient.desktopUI.toast.success('Hello, Docker Community');
  }

  return (
    <Grid
      container
      direction="column"
      justifyContent="center"
      alignItems="center"
      height="100vh"
    >
      <Grid item>
        <Typography variant="h1" gutterBottom>
          Welcome to the Docker Extension CheatSheet
        </Typography>
      </Grid>
      <Grid item>
        <Button variant="contained" onClick={sayHello}>
          Click me!
        </Button>
      </Grid>
    </Grid>
  );
}
Enter fullscreen mode Exit fullscreen mode

Image11

The Material Design responsive layout grid adapts to screen size and orientation, ensuring consistency across layouts.

Image12

Building Docker Extension CheatSheet

import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import { Grid, Button, Typography } from '@mui/material';
import { createDockerDesktopClient } from '@docker/extension-api-client';

export function App() {
  const ddClient = createDockerDesktopClient();

  function sayHello() {
    console.log("Hi Console")
    ddClient.desktopUI.toast.success('Welcome to Docker Extensions CheatSheet');
  }

  return (
    <Grid container spacing={2}>
      <Grid item xs={12}>
        <Typography variant="h1" gutterBottom>
          Welcome to the Docker Extension Cheatsheet
        </Typography>
        <Button variant="contained" onClick={sayHello}>
          Click me!
        </Button>
      </Grid>
      <Grid item xs={12}>
        <TableContainer component={Paper}>
          <Table aria-label="cheatsheet table">
            <TableHead>
              <TableRow>
                <TableCell>Commands</TableCell>
                <TableCell>Command</TableCell>
                <TableCell>Description</TableCell>
              </TableRow>
            </TableHead>
            <TableBody>
              <TableRow>
                <TableCell>Listing</TableCell>
                <TableCell>docker extension ls</TableCell>
                <TableCell>List all Docker Extensions</TableCell>
              </TableRow>
              <TableRow>
                <TableCell>Initializing</TableCell>
                <TableCell>docker extension init</TableCell>
                <TableCell>Create a new Docker Extension based on a template</TableCell>
              </TableRow>
              <TableRow>
                <TableCell>Installing</TableCell>
                <TableCell>docker extension install &lt;extension-name&gt;</TableCell>
                <TableCell>Install a Docker extension with the specified image</TableCell>
              </TableRow>
              <TableRow>
                <TableCell>Removing</TableCell>
                <TableCell>docker extension rm &lt;extension-name&gt;</TableCell>
                <TableCell>Remove a Docker extension</TableCell>
              </TableRow>
              <TableRow>
                <TableCell>Validating</TableCell>
                <TableCell>docker extension validate &lt;extension-name&gt;</TableCell>
                <TableCell>Validate an extension image or metadata file</TableCell>
              </TableRow>
              <TableRow>
                <TableCell>Sharing</TableCell>
                <TableCell>docker extension share &lt;extension-namegt;</TableCell>
                <TableCell>Generate a link to share the extension</TableCell>
              </TableRow>
            </TableBody>
          </Table>
        </TableContainer>
      </Grid>
    </Grid>
  );
}
Enter fullscreen mode Exit fullscreen mode

Image13

References:

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .