In this blog I will answer the following question: What happens inside Docker during a docker run command?
To get started let's clone the Docker Github repo and inspect the code.
git clone https://github.com/docker/docker
Open the project using your favorite editor or IDE and look around in the main tree. Docker is written in Golang and consists of many packages. For instance, from top to bottom you see api, builder, builtins, contrib and so on. Some folders contain subfolders with more packages.
First things first, let's find func main, the Golang function that is executed when we run the Docker executable. There are actually more than 30 main funcs in the code tree. These are for all sorts of utilties which I won't go into right now. Let's continue with the main we are looking for: the one that lives in docker/docker.go. Let's look at it more closely.
See the snippet below, showing the first dozen lines of the main func. When Docker starts it runs any initializers via reexec, if any, then it parses the flags for the Docker executable via the mflag package. This packages lives under pkg/mgflag and is aliased as flag. At this point it can print out version information if necessary or enable debug mode and debug logging.
func main() {
if reexec.Init() {
return
}
flag.Parse()
// FIXME: validate daemon flags here
if *flVersion {
showVersion()
return
}
if *flDebug {
os.Setenv("DEBUG", "1")
}
initLogging(*flDebug)
// ... SNIP
}
After option parsing Docker captures hosts settings and performs TLS verification for the server, if necessary. This happens between line 40 and 107. See the snippet below. The flags that were parsed earlier are passed to the Cmd method from the DockerCli type in api/client/cli.go. If an error occurs it is logged and the program is exited.
func main() {
// ... SNIP
if err := cli.Cmd(flag.Args()...); err != nil {
if sterr, ok := err.(*utils.StatusError); ok {
if sterr.Status != "" {
log.Println("%s", sterr.Status)
}
os.Exit(sterr.StatusCode)
}
log.Fatal(err)
}
}
Let's dive into the cli package to see how the Docker commands are being handled. To be able to run the subcommands we look at 3 things:
The DockerCli struct contains datastructures each Docker command requires such as the protocol that is used, in-, output- and error writers as wel TLS specific data structures.
type DockerCli struct {
proto string
addr string
configFile *registry.ConfigFile
in io.ReadCloser
out io.Writer
err io.Writer
key libtrust.PrivateKey
tlsConfig *tls.Config
scheme string
// inFd holds file descriptor of the client's STDIN, if it's a valid file
inFd uintptr
// outFd holds file descriptor of the client's STDOUT, if it's a valid file
outFd uintptr
// isTerminalIn describes if client's STDIN is a TTY
isTerminalIn bool
// isTerminalOut describes if client's STDOUT is a TTY
isTerminalOut bool
transport *http.Transport
}
The Cmd func's responsibility is to translate the command arguments to a function using the getMethod func. It already supports multiple commands for the future, possibly docker groups create, although as far as I know no such commands have been implemented yet.
func (cli *DockerCli) Cmd(args ...string) error {
if len(args) > 1 {
method, exists := cli.getMethod(args[:2]...)
if exists {
return method(args[2:]...)
}
}
if len(args) > 0 {
method, exists := cli.getMethod(args[0])
if !exists {
fmt.Println("Error: Command not found:", args[0])
return cli.CmdHelp(args[1:]...)
}
return method(args[1:]...)
}
return cli.CmdHelp(args...)
}
Notice the getMethod is lowercased. This means it is not exported to outside packages so it's only available from within cli. So how does this method find the correct func? See the snippet below. It first builds up a string starting with Cmd concatenated with the capitalized arguments. In case of a Docker run the methodName variable will be CmdRun. Using the MethodByName func from Golang's reflect package it retrieves a function pointer and returns it.
func (cli *DockerCli) getMethod(args ...string) (func(...string) error, bool) {
camelArgs := make([]string, len(args))
for i, s := range args {
if len(s) == 0 {
return nil, false
}
camelArgs[i] = strings.ToUpper(s[:1]) + strings.ToLower(s[1:])
}
methodName := "Cmd" + strings.Join(camelArgs, "")
fmt.Println(methodName)
method := reflect.ValueOf(cli).MethodByName(methodName)
if !method.IsValid() {
return nil, false
}
return method.Interface().(func(...string) error), true
}
Finally we arrive at the func responsible for running a container: CmdRun at api/client/commands.go. This file contains all Docker commands. Arguments for the run itself are now parsed, such as the image, the command and other arguments. Since we have already been through that I won't show that code. Instead I show something more interesting: starting a new container to run the command in.
The snippet below shows how the container is created. The configuration of the container is merged from the run config, and the host config.
The actual call to create the container is a HTTP POST to the Docker server.
func (cli *DockerCli) CmdRun(args ...string) error {
// SNIP ...
runResult, err := cli.createContainer(config, hostConfig, hostConfig.ContainerIDFile, *flName)
if err != nil {
return err
}
// SNIP ...
}
func (cli *DockerCli) createContainer(config *runconfig.Config, hostConfig *runconfig.HostConfig, cidfile, name string) (engine.Env, error) {
containerValues := url.Values{}
if name != "" {
containerValues.Set("name", name)
}
mergedConfig := runconfig.MergeConfigs(config, hostConfig)
var containerIDFile *cidFile
if cidfile != "" {
var err error
if containerIDFile, err = newCIDFile(cidfile); err != nil {
return nil, err
}
defer containerIDFile.Close()
}
//create the container
stream, statusCode, err := cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, false)
//if image not found try to pull it
if statusCode == 404 {
fmt.Fprintf(cli.err, "Unable to find image '%s' locallyn", config.Image)
// we don't want to write to stdout anything apart from container.ID
if err = cli.pullImageCustomOut(config.Image, cli.err); err != nil {
return nil, err
}
// Retry
if stream, _, err = cli.call("POST", "/containers/create?"+containerValues.Encode(), mergedConfig, false); err != nil {
return nil, err
}
} else if err != nil {
return nil, err
}
var result engine.Env
if err := result.Decode(stream); err != nil {
return nil, err
}
for _, warning := range result.GetList("Warnings") {
fmt.Fprintf(cli.err, "WARNING: %sn", warning)
}
if containerIDFile != nil {
if err = containerIDFile.Write(result.Get("Id")); err != nil {
return nil, err
}
}
return result, nil
}
This covers what's happening inside the Docker client. There is of course a lot more code to be explored in the Docker server and in libcontainer but that will be left to a future blog post.