keepcalculator

06 Numbers and Powers

Learn Go by Building a Docker CLI Tool

Card No. 06·48794 · Updated · Standard formula, shown below

This guide walks you through learning Go by building a command-line tool that interacts with Docker. You’ll create a program that lists containers, shows their stats, and can start or stop them—all from your terminal. By the end, you’ll have a working CLI and a solid grasp of Go’s core concepts: structs, HTTP clients, JSON parsing, and flag handling.

What You’ll Need

  • Go installed (version 1.21 or later recommended). Check with go version.
  • Docker installed and running. Verify with docker ps.
  • Access to Docker’s Unix socket (usually /var/run/docker.sock). On Linux you may need to be in the docker group.
  • A text editor or IDE (VS Code, GoLand, or even Vim).

Step-by-Step

  1. Set up your project
    Create a new directory and initialize a Go module:
    mkdir docker-cli
    cd docker-cli
    go mod init docker-cli
  2. Understand Docker’s API
    Docker exposes a REST API on a Unix socket. For example, GET /containers/json returns a list of containers. You’ll access this via Go’s net/http client configured to use the socket.
  3. Create the main file
    Create main.go with a basic structure:
    package main
    
    import (
        "fmt"
        "os"
    )
    
    func main() {
        if len(os.Args) < 2 {
            fmt.Println("Usage: docker-cli [list|stats|start|stop]")
            os.Exit(1)
        }
        // command dispatch will go here
    }
  4. Define the Docker client
    Create a function that returns an http.Client configured to talk to the Docker socket:
    func newDockerClient() *http.Client {
        return &http.Client{
            Transport: &http.Transport{
                DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) {
                    return net.Dial("unix", "/var/run/docker.sock")
                },
            },
        }
    }
  5. Define a Container struct
    Parse the JSON response from Docker into a struct. Start with the fields you need:
    type Container struct {
        ID     string   `json:"Id"`
        Names  []string `json:"Names"`
        Image  string   `json:"Image"`
        State  string   `json:"State"`
        Status string   `json:"Status"`
    }
  6. Implement the list command
    Write a function that fetches /containers/json and prints the containers:
    func listContainers(client *http.Client) {
        resp, err := client.Get("http://docker/containers/json")
        if err != nil { panic(err) }
        defer resp.Body.Close()
        var containers []Container
        json.NewDecoder(resp.Body).Decode(&containers)
        for _, c := range containers {
            fmt.Printf("%s\t%s\t%s\n", c.ID[:12], c.Image, c.Status)
        }
    }

    Note: the hostname in the URL is ignored when using a Unix socket, but must be present.

  7. Add the stats command
    For live stats, use /containers/{id}/stats?stream=false. Define a struct for the CPU and memory usage:
    type Stats struct {
        CPUStats struct {
            CPUUsage struct {
                TotalUsage uint64 `json:"total_usage"`
            } `json:"cpu_usage"`
            SystemCPUUsage uint64 `json:"system_cpu_usage"`
        } `json:"cpu_stats"`
        MemoryStats struct {
            Usage uint64 `json:"usage"`
            Limit uint64 `json:"limit"`
        } `json:"memory_stats"`
    }

    Then calculate CPU percentage as (deltaTotal / deltaSystem) * 100.

  8. Implement start and stop
    Use POST /containers/{id}/start and POST /containers/{id}/stop. You’ll need to create requests with the correct method and call client.Do(req).
  9. Wire up the command dispatch
    In main(), use a switch statement to call the appropriate function based on os.Args[1]. For start and stop, require a container ID as the second argument.
  10. Test your tool
    Run go build to compile, then try ./docker-cli list. You should see your running containers. If you have none, start one with docker run -d nginx first.
  11. Add flags for filtering
    Use the flag package to support -a (show all containers, not just running) and -n (limit number of results). Pass these as query parameters to the API.

Common Mistakes

  • Forgetting the socket path: If you’re not on Linux, the Docker socket might be at a different location (e.g., ~/.docker/run/docker.sock on macOS with Docker Desktop). Make it configurable.
  • Ignoring error responses: Docker returns 404 if a container doesn’t exist, 409 if it’s already started. Always check resp.StatusCode.
  • Misparsing JSON field names: Docker uses Id (capital I) not ID. Your struct tag must match exactly.
  • Not handling the body close: Always defer resp.Body.Close() to avoid resource leaks.
  • Assuming the socket is always available: Check if the socket file exists before trying to connect, and give a friendly error if not.

Tips

  • Use the official Docker API documentation (docs.docker.com/engine/api) as your reference. The versioned endpoints (e.g., /v1.41/containers/json) are more stable.
  • Start with just the list command, get it working, then add others. This keeps the learning curve manageable.
  • Use encoding/json with structs rather than map[string]interface{}—it’s more readable and type-safe.
  • Make the socket path a global variable so you can override it with an environment variable like DOCKER_HOST.
  • Add a --help flag using the flag package to make your tool more user-friendly.
  • Write a few unit tests for your JSON parsing using sample responses from the Docker API docs.

Troubleshooting

  • “permission denied” on the socket: Add your user to the docker group (sudo usermod -aG docker $USER) and log out/in, or run the tool with sudo for testing.
  • “connection refused”: Docker isn’t running, or the socket path is wrong. Check with docker ps and adjust the path.
  • Empty list when containers exist: You’re probably only showing running containers. Use the -a flag or add ?all=1 to the API call.
  • JSON decode errors: The API response might have fields you haven’t defined. Use json.Decoder with DisallowUnknownFields() to catch this, or add the missing fields to your struct.
  • Start/stop not working: Check that you’re using the correct HTTP method (POST) and that the container ID is correct. Use fmt.Println(req.URL) to debug.

Frequently Asked Questions

Do I need to know Go before starting?

Basic familiarity helps, but this project is designed to teach as you go. You’ll learn structs, HTTP, and JSON handling by doing.

Can I use this with Kubernetes instead?

Yes, but the Kubernetes API is more complex (authentication, namespaces, etc.). Docker’s socket is simpler for learning. You can apply the same patterns later.

Why use a Unix socket instead of HTTP?

Docker’s daemon listens on a Unix socket by default for security—it’s only accessible to local users. This avoids exposing a network port.

How do I handle container names with spaces?

Docker names don’t have spaces, but they may have leading slashes (e.g., /my-container). Strip the slash with strings.TrimPrefix.

Can I add a subcommand like logs?

Absolutely. The pattern is the same: call /containers/{id}/logs and print the response body. That’s a great next step to practice.

Is this safe to run in production?

It’s a learning tool. For production, consider using an official Docker client library for Go, which handles edge cases and versioning better.

Related

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →