mirror of https://github.com/docker/docs.git
533 lines
21 KiB
Markdown
533 lines
21 KiB
Markdown
---
|
||
title: "Build your Go image"
|
||
keywords: containers, images, go, golang, dockerfiles, coding, build, push, run
|
||
description: Learn how to build your first Docker image by writing a Dockerfile
|
||
redirect_from:
|
||
- /get-started/golang/build-images/
|
||
---
|
||
|
||
{% include_relative nav.html selected="1" %}
|
||
|
||
## Overview
|
||
|
||
In this section we are going to build a container image. The image includes everything you need
|
||
to run your application – the compiled application binary file, the runtime, the libraries, and
|
||
all other resources required by your application.
|
||
|
||
## Required software
|
||
|
||
To complete this tutorial, you need the following:
|
||
|
||
- Go version 1.19 or later. Visit the [download page for Go](https://golang.org/dl/){:target="_blank" rel="noopener" class="_"} first and install the toolchain.
|
||
- Docker running locally. Follow the [instructions to download and install Docker](../../desktop/index.md).
|
||
- An IDE or a text editor to edit files. [Visual Studio Code](https://code.visualstudio.com/){: target="_blank" rel="noopener" class="_"} is a free and popular choice but you can use anything you feel comfortable with.
|
||
- A Git client. We'll use a command-line based `git` client throughout this module, but you are free to use whatever works for you.
|
||
- A command-line terminal application. The examples shown in this module are from the Linux shell, but they should work in PowerShell, Windows Command Prompt, or OS X Terminal with minimal, if any, modifications.
|
||
|
||
## Meet the example application
|
||
|
||
The example application is a *caricature* of a microservice. It is purposefully trivial to keep focus on learning the basics of containerization for Go applications.
|
||
|
||
The application offers two HTTP endpoints:
|
||
|
||
* It responds with a string containing a heart symbol (`<3`) to requests to `/`.
|
||
* It responds with `{"Status" : "OK"}` JSON to a request to `/health`.
|
||
|
||
It responds with HTTP error 404 to any other request.
|
||
|
||
The application listens on a TCP port defined by the value of environment variable `PORT`. The default value is `8080`.
|
||
|
||
The application is *stateless*.
|
||
|
||
The complete source code for the application is on GitHub: [github.com/docker/docker-gs-ping](https://github.com/docker/docker-gs-ping){: target="_blank" rel="noopener" class="_"}. You are encouraged to fork it and experiment with it as much as you like.
|
||
|
||
To continue, we clone the application repository to our local machine:
|
||
|
||
```console
|
||
$ git clone https://github.com/docker/docker-gs-ping
|
||
```
|
||
|
||
The application's `main.go` file is fairly straightforward, if you are familiar with Go:
|
||
|
||
{% raw %}
|
||
```go
|
||
package main
|
||
|
||
import (
|
||
"net/http"
|
||
"os"
|
||
|
||
"github.com/labstack/echo/v4"
|
||
"github.com/labstack/echo/v4/middleware"
|
||
)
|
||
|
||
func main() {
|
||
|
||
e := echo.New()
|
||
|
||
e.Use(middleware.Logger())
|
||
e.Use(middleware.Recover())
|
||
|
||
e.GET("/", func(c echo.Context) error {
|
||
return c.HTML(http.StatusOK, "Hello, Docker! <3")
|
||
})
|
||
|
||
e.GET("/health", func(c echo.Context) error {
|
||
return c.JSON(http.StatusOK, struct{ Status string }{Status: "OK"})
|
||
})
|
||
|
||
httpPort := os.Getenv("PORT")
|
||
if httpPort == "" {
|
||
httpPort = "8080"
|
||
}
|
||
|
||
e.Logger.Fatal(e.Start(":" + httpPort))
|
||
}
|
||
|
||
// Simple implementation of an integer minimum
|
||
// Adapted from: https://gobyexample.com/testing-and-benchmarking
|
||
func IntMin(a, b int) int {
|
||
if a < b {
|
||
return a
|
||
}
|
||
return b
|
||
}
|
||
```
|
||
{% endraw %}
|
||
|
||
## Smoke test the application
|
||
|
||
Let’s start our application and make sure it’s running properly. Open your
|
||
terminal and navigate to the directory into which you cloned the project's repo.
|
||
From now on, we'll refer to this directory as the **project directory**.
|
||
|
||
```console
|
||
$ go run main.go
|
||
```
|
||
|
||
This should compile and start the server as a foreground application, outputting
|
||
the banner, as illustrated in the next figure.
|
||
|
||
```
|
||
____ __
|
||
/ __/___/ / ___
|
||
/ _// __/ _ \/ _ \
|
||
/___/\__/_//_/\___/ v4.10.2
|
||
High performance, minimalist Go web framework
|
||
https://echo.labstack.com
|
||
____________________________________O/_______
|
||
O\
|
||
⇨ http server started on [::]:8080
|
||
```
|
||
|
||
Let's run a quick _smoke test_ by accessing the application on `http://localhost:8080`.
|
||
You can use your favourite web browser, or even a `curl` command in the terminal:
|
||
|
||
```console
|
||
$ curl http://localhost:8080/
|
||
Hello, Docker! <3
|
||
```
|
||
|
||
This verifies that the application builds locally and we can start it without an error.
|
||
That's a milestone to celebrate!
|
||
|
||
Now we are ready to "containerize" it.
|
||
|
||
## Create a Dockerfile for the application
|
||
|
||
To build a container image with Docker, a *Dockerfile* with build instructions is required.
|
||
|
||
We begin our `Dockerfile` with the (optional) parser directive line that instructs BuildKit to
|
||
interpret our file according to the grammar rules for the specified version of the syntax.
|
||
|
||
We then tell Docker what *base image* we would like to use for our application:
|
||
|
||
```dockerfile
|
||
# syntax=docker/dockerfile:1
|
||
|
||
FROM golang:1.19
|
||
```
|
||
|
||
Docker images can be inherited from other images. Therefore, instead of creating
|
||
our own base image from scratch, we can use the official Go image that already has
|
||
all necessary tools and libraries to compile and run a Go application.
|
||
|
||
> **Note**
|
||
>
|
||
> If you are curious about creating your own base images, you can check out the following section of this guide: [creating base images](../../build/building/base-images.md).
|
||
> Note, however, that this is not necessary to continue with our task at hand.
|
||
|
||
Now that we have defined the "base" image for our upcoming container image,
|
||
we can begin building on top of it.
|
||
|
||
To make things easier when running the rest of our commands, let’s create a
|
||
directory _inside_ the image that we are building. This also instructs Docker
|
||
to use this directory as the default _destination_ for all subsequent commands.
|
||
This way we do not have to type out full file paths in the `Dockerfile`,
|
||
the relative paths will be based on this directory.
|
||
|
||
```dockerfile
|
||
WORKDIR /app
|
||
```
|
||
|
||
Usually the very first thing you do once you’ve downloaded a project written in
|
||
Go is to install the modules necessary to compile it. Note, that the base image
|
||
has the toolchain already, but our source code is not in it yet.
|
||
|
||
So before we can run `go mod download` inside our image, we need to get our
|
||
`go.mod` and `go.sum` files copied into it. We use the `COPY` command to do this.
|
||
|
||
In its simplest form, the `COPY` command takes two parameters. The first
|
||
parameter tells Docker what files you want to copy into the image. The last
|
||
parameter tells Docker where you want that file to be copied to.
|
||
|
||
We’ll copy the `go.mod` and `go.sum` file into our project directory `/app` which,
|
||
owing to our use of `WORKDIR`, is the current directory (`./`) inside the image.
|
||
Unlike some modern shells that appear to be indifferent to the use of trailing slash (`/`),
|
||
and can figure out what the user meant (most of the time), Docker's `COPY` command
|
||
is quite sensitive in its interpretation of the trailing slash.
|
||
|
||
```dockerfile
|
||
COPY go.mod go.sum ./
|
||
```
|
||
|
||
> **Notice**
|
||
>
|
||
> Please take some time to familiarise yourself with the trailing slash treatment
|
||
> by the `COPY` command: [Dockerfile reference](../../engine/reference/builder.md/#copy)
|
||
> as it might otherwise trick you up in more ways than you can imagine.
|
||
|
||
Now that we have the module files inside the Docker image that we are building,
|
||
we can use the `RUN` command to execute the command `go mod download` there as
|
||
well. This works exactly the same as if we were running `go` locally on our
|
||
machine, but this time these Go modules will be installed into a directory
|
||
inside the image.
|
||
|
||
```dockerfile
|
||
RUN go mod download
|
||
```
|
||
|
||
At this point, we have a Go toolchain version 1.19.x and all our Go dependencies
|
||
installed inside the image.
|
||
|
||
The next thing we need to do is to copy our source code into the image. We’ll
|
||
use the `COPY` command just like we did with our module files before.
|
||
|
||
```dockerfile
|
||
COPY *.go ./
|
||
```
|
||
|
||
This `COPY` command uses a wildcard to copy all files with `.go` extension
|
||
located in the current directory on the host (the directory where the `Dockerfile`
|
||
is located) into the current directory inside the image.
|
||
|
||
Now, we would like to compile our application. To that end, we use the familiar
|
||
`RUN` command:
|
||
|
||
```dockerfile
|
||
RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping
|
||
```
|
||
|
||
This should be familiar. The result of that command will be a static application
|
||
binary named `docker-gs-ping` and located in the root of the filesystem of the
|
||
image that we are building. We could have put the binary into any other place we
|
||
desire inside that image, the root directory has no special meaning in this
|
||
regard. It's just convenient to use it to keep the file paths short for improved
|
||
readability.
|
||
|
||
Now, all that is left to do is to tell Docker what command to execute when our
|
||
image is used to start a container.
|
||
|
||
We do this with the `CMD` command:
|
||
|
||
```dockerfile
|
||
CMD ["/docker-gs-ping"]
|
||
```
|
||
|
||
Here's the complete `Dockerfile`:
|
||
|
||
```dockerfile
|
||
# syntax=docker/dockerfile:1
|
||
|
||
FROM golang:1.19
|
||
|
||
# Set destination for COPY
|
||
WORKDIR /app
|
||
|
||
# Download Go modules
|
||
COPY go.mod go.sum ./
|
||
RUN go mod download
|
||
|
||
# Copy the source code. Note the slash at the end, as explained in
|
||
# https://docs.docker.com/engine/reference/builder/#copy
|
||
COPY *.go ./
|
||
|
||
# Build
|
||
RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping
|
||
|
||
# Optional:
|
||
# To bind to a TCP port, runtime parameters must be supplied to the docker command.
|
||
# But we can document in the Dockerfile what ports
|
||
# the application is going to listen on by default.
|
||
# https://docs.docker.com/engine/reference/builder/#expose
|
||
EXPOSE 8080
|
||
|
||
# Run
|
||
CMD ["/docker-gs-ping"]
|
||
```
|
||
|
||
The `Dockerfile` may also contain _comments_. They always begin with a `#` symbol,
|
||
and must be at the beginning of a line. Comments are there for your convenience
|
||
to allow documenting your `Dockerfile`.
|
||
|
||
There is also a concept of Dockerfile _directives_, such as the `syntax` directive we added.
|
||
The directives must always be at the very top of the `Dockerfile`, so when adding comments,
|
||
make sure that the comments follow *after* any directives that you may have used:
|
||
|
||
```dockerfile
|
||
# syntax=docker/dockerfile:1
|
||
# A sample microservice in Go packaged into a container image.
|
||
|
||
FROM golang:1.19
|
||
|
||
# ...
|
||
```
|
||
|
||
## Build the image
|
||
|
||
Now that we've created our `Dockerfile`, let’s build an image from it. The
|
||
`docker build` command creates Docker images from the `Dockerfile` and a "context".
|
||
A build _context_ is the set of files located in the specified path or URL. The
|
||
Docker build process can access any of the files located in the context.
|
||
|
||
The build command optionally takes a `--tag` flag. This flag is used to label
|
||
the image with a string value, which is easy for humans to read and recognise.
|
||
If you do not pass a `--tag`, Docker will use `latest` as the default value.
|
||
|
||
Let's build our first Docker image!
|
||
|
||
```console
|
||
$ docker build --tag docker-gs-ping .
|
||
```
|
||
|
||
The build process will print some diagnostic messages as it goes through the build steps.
|
||
The following is just an example of what these messages may look like.
|
||
|
||
```console
|
||
[+] Building 2.2s (15/15) FINISHED
|
||
=> [internal] load build definition from Dockerfile 0.0s
|
||
=> => transferring dockerfile: 701B 0.0s
|
||
=> [internal] load .dockerignore 0.0s
|
||
=> => transferring context: 2B 0.0s
|
||
=> resolve image config for docker.io/docker/dockerfile:1 1.1s
|
||
=> CACHED docker-image://docker.io/docker/dockerfile:1@sha256:39b85bbfa7536a5feceb7372a0817649ecb2724562a38360f4d6a7782a409b14 0.0s
|
||
=> [internal] load build definition from Dockerfile 0.0s
|
||
=> [internal] load .dockerignore 0.0s
|
||
=> [internal] load metadata for docker.io/library/golang:1.19 0.7s
|
||
=> [1/6] FROM docker.io/library/golang:1.19@sha256:5d947843dde82ba1df5ac1b2ebb70b203d106f0423bf5183df3dc96f6bc5a705 0.0s
|
||
=> [internal] load build context 0.0s
|
||
=> => transferring context: 6.08kB 0.0s
|
||
=> CACHED [2/6] WORKDIR /app 0.0s
|
||
=> CACHED [3/6] COPY go.mod go.sum ./ 0.0s
|
||
=> CACHED [4/6] RUN go mod download 0.0s
|
||
=> CACHED [5/6] COPY *.go ./ 0.0s
|
||
=> CACHED [6/6] RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping 0.0s
|
||
=> exporting to image 0.0s
|
||
=> => exporting layers 0.0s
|
||
=> => writing image sha256:ede8ff889a0d9bc33f7a8da0673763c887a258eb53837dd52445cdca7b7df7e3 0.0s
|
||
=> => naming to docker.io/library/docker-gs-ping 0.0s
|
||
```
|
||
|
||
Your exact output will vary, but provided there aren't any errors, you should
|
||
see the word `FINISHED` in the first line of output. This means Docker has successfully
|
||
built our image named `docker-gs-ping`.
|
||
|
||
## View local images
|
||
|
||
To see the list of images we have on our local machine, we have two options. One
|
||
is to use the CLI and the other is to use [Docker Desktop](../../desktop/index.md).
|
||
Since we are currently working in the terminal, let’s take a look at listing
|
||
images with the CLI.
|
||
|
||
To list images, run the `docker image ls`command (or the `docker images` shorthand):
|
||
|
||
```console
|
||
$ docker image ls
|
||
|
||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||
docker-gs-ping latest 7f153fbcc0a8 2 minutes ago 1.11GB
|
||
...
|
||
```
|
||
|
||
Your exact output may vary, but you should see the `docker-gs-ping` image with the
|
||
`latest` tag. Because we had not specified a custom tag when we built our image,
|
||
Docker assumed that the tag would be `latest`, which is a special value.
|
||
|
||
## Tag images
|
||
|
||
An image name is made up of slash-separated name components. Name components may
|
||
contain lowercase letters, digits and separators. A separator is defined as a
|
||
period, one or two underscores, or one or more dashes. A name component may not
|
||
start or end with a separator.
|
||
|
||
An image is made up of a manifest and a list of layers. In simple terms, a “tag”
|
||
points to a combination of these artifacts. You can have multiple tags for the
|
||
image and, in fact, most images have multiple tags. Let’s create a second tag
|
||
for the image we had built and take a look at its layers.
|
||
|
||
Use the `docker image tag` (or `docker tag` shorthand) command to create a new
|
||
tag for our image. This command takes two arguments; the first argument is the
|
||
"source" image, and the second is the new tag to create. The following command
|
||
creates a new `docker-gs-ping:v1.0` tag for the `docker-gs-ping:latest` we built
|
||
above:
|
||
|
||
```console
|
||
$ docker image tag docker-gs-ping:latest docker-gs-ping:v1.0
|
||
```
|
||
|
||
The Docker `tag` command creates a new tag for the image. It does not create a
|
||
new image. The tag points to the same image and is just another way to reference
|
||
the image.
|
||
|
||
Now run the `docker image ls` command again to see the updated list of local
|
||
images:
|
||
|
||
```console
|
||
$ docker image ls
|
||
|
||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||
docker-gs-ping latest 7f153fbcc0a8 6 minutes ago 1.11GB
|
||
docker-gs-ping v1.0 7f153fbcc0a8 6 minutes ago 1.11GB
|
||
...
|
||
```
|
||
|
||
You can see that we have two images that start with `docker-gs-ping`. We know
|
||
they are the same image because if you look at the `IMAGE ID` column, you can
|
||
see that the values are the same for the two images. This value is a unique
|
||
identifier Docker uses internally to identify the image.
|
||
|
||
Let’s remove the tag that we had just created. To do this, we’ll use the
|
||
`docker image rm` command, or the shorthand `docker rmi` (which stands for
|
||
"remove image"):
|
||
|
||
```console
|
||
$ docker image rm docker-gs-ping:v1.0
|
||
Untagged: docker-gs-ping:v1.0
|
||
```
|
||
|
||
Notice that the response from Docker tells us that the image has not been removed but only "untagged".
|
||
|
||
Verify this by running the following command:
|
||
|
||
```console
|
||
$ docker image ls
|
||
```
|
||
|
||
You will see that the tag `v1.0` is no longer in the list of images kept by your Docker instance.
|
||
|
||
```
|
||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||
docker-gs-ping latest 7f153fbcc0a8 7 minutes ago 1.11GB
|
||
...
|
||
```
|
||
|
||
The tag `v1.0` has been removed but we still have the `docker-gs-ping:latest`
|
||
tag available on our machine, so the image is there.
|
||
|
||
## Multi-stage builds
|
||
|
||
You may have noticed that our `docker-gs-ping` image weighs in at over a gigabyte (!!!),
|
||
which is *a lot* for a tiny compiled Go application. You may also be wondering what happened
|
||
to the full suite of Go tools, including the compiler, after we had built our image.
|
||
|
||
The answer is that the full toolchain is still there, in the container image.
|
||
Not only this is inconvenient because of the large file size, but it may also
|
||
present a security risk when the container is deployed.
|
||
|
||
These two issues can be solved by using [multi-stage builds](../../build/building/multi-stage.md).
|
||
|
||
In a nutshell, a multi-stage build can carry over the artifacts from one build stage into another,
|
||
and every build stage can be instantiated from a different base image.
|
||
|
||
Thus, in the following example, we are going to use a full-scale official Go image to build
|
||
our application but then we'll copy the application binary into another image whose base
|
||
is very lean and does not include the Go toolchain or other optional components.
|
||
|
||
The `Dockerfile.multistage` in the sample application's repo has the following
|
||
content:
|
||
|
||
{% raw %}
|
||
```dockerfile
|
||
# syntax=docker/dockerfile:1
|
||
|
||
# Build the application from source
|
||
FROM golang:1.19 AS build-stage
|
||
|
||
WORKDIR /app
|
||
|
||
COPY go.mod go.sum ./
|
||
RUN go mod download
|
||
|
||
COPY *.go ./
|
||
|
||
RUN CGO_ENABLED=0 GOOS=linux go build -o /docker-gs-ping
|
||
|
||
# Run the tests in the container
|
||
FROM build-stage AS run-test-stage
|
||
RUN go test -v ./...
|
||
|
||
# Deploy the application binary into a lean image
|
||
FROM gcr.io/distroless/base-debian11 AS build-release-stage
|
||
|
||
WORKDIR /
|
||
|
||
COPY --from=build-stage /docker-gs-ping /docker-gs-ping
|
||
|
||
EXPOSE 8080
|
||
|
||
USER nonroot:nonroot
|
||
|
||
ENTRYPOINT ["/docker-gs-ping"]
|
||
```
|
||
{% endraw %}
|
||
|
||
Since we have two Dockerfiles now, we have to tell Docker what Dockerfile we'd like to use
|
||
to build the image. Let's tag the new image with `multistage`. This tag (like any other,
|
||
apart from `latest`) has no special meaning for Docker, it's just something we chose.
|
||
|
||
```console
|
||
$ docker build -t docker-gs-ping:multistage -f Dockerfile.multistage .
|
||
```
|
||
|
||
Comparing the sizes of `docker-gs-ping:multistage` and `docker-gs-ping:latest`
|
||
we see a *few orders-of-magnitude* difference! (`docker image ls`)
|
||
|
||
```
|
||
REPOSITORY TAG IMAGE ID CREATED SIZE
|
||
docker-gs-ping multistage e3fdde09f172 About a minute ago 28.1MB
|
||
docker-gs-ping latest 336a3f164d0f About an hour ago 1.11GB
|
||
```
|
||
|
||
This is so because the ["distroless"](https://github.com/GoogleContainerTools/distroless){:target="_blank" rel="noopener" class="_"}
|
||
base image that we have used in the second stage of the build is very barebones and is designed for lean deployments of static binaries.
|
||
|
||
There's much more to multi-stage builds, including the possibility of multi-architecture builds,
|
||
so please feel free to check out the [multi-stage builds](../../build/building/multi-stage.md)
|
||
section of Docker documentation. This is, however, not essential for our progress here, so we'll
|
||
leave it at that.
|
||
|
||
## Next steps
|
||
|
||
In this module, we met our example application and built and container image for it.
|
||
|
||
In the next module, we’ll take a look at how to:
|
||
|
||
[Run your image as a container](run-containers.md){: .button .primary-btn}
|
||
|
||
## Feedback
|
||
|
||
Help us improve this topic by providing your feedback. Let us know what you
|
||
think by creating an issue in the [Docker Docs]({{ site.repo }}/issues/new?title=[Golang%20docs%20feedback]){:target="_blank" rel="noopener" class="_"}
|
||
GitHub repository. Alternatively, [create a PR]({{ site.repo }}/pulls){:target="_blank" rel="noopener" class="_"}
|
||
to suggest updates.
|