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 thedockergroup. - A text editor or IDE (VS Code, GoLand, or even Vim).
Step-by-Step
- Set up your project
Create a new directory and initialize a Go module:mkdir docker-cli cd docker-cli go mod init docker-cli - Understand Docker’s API
Docker exposes a REST API on a Unix socket. For example,GET /containers/jsonreturns a list of containers. You’ll access this via Go’snet/httpclient configured to use the socket. - Create the main file
Createmain.gowith 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 } - Define the Docker client
Create a function that returns anhttp.Clientconfigured 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") }, }, } } - 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"` } - Implement the list command
Write a function that fetches/containers/jsonand 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.
- 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. - Implement start and stop
UsePOST /containers/{id}/startandPOST /containers/{id}/stop. You’ll need to create requests with the correct method and callclient.Do(req). - Wire up the command dispatch
Inmain(), use a switch statement to call the appropriate function based onos.Args[1]. Forstartandstop, require a container ID as the second argument. - Test your tool
Rungo buildto compile, then try./docker-cli list. You should see your running containers. If you have none, start one withdocker run -d nginxfirst. - Add flags for filtering
Use theflagpackage 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.sockon 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) notID. 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
listcommand, get it working, then add others. This keeps the learning curve manageable. - Use
encoding/jsonwith structs rather thanmap[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
--helpflag using theflagpackage 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
dockergroup (sudo usermod -aG docker $USER) and log out/in, or run the tool withsudofor testing. - “connection refused”: Docker isn’t running, or the socket path is wrong. Check with
docker psand adjust the path. - Empty list when containers exist: You’re probably only showing running containers. Use the
-aflag or add?all=1to the API call. - JSON decode errors: The API response might have fields you haven’t defined. Use
json.DecoderwithDisallowUnknownFields()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
- Jenkins vs GitHub Actions: Which CI/CD Fits Your Team?
- Turn Off Backup Encryption in iTunes?
- Campus Tech Innovation Hubs: What They Are
- Poco C51 Airtel Prepaid Locked Variant India?
- Shared VPS Hosting: Is It Better?
- Disk Defrag Pro: What It Does and Whether You Need It