I have been using using Docker and Kubernetes off and on for work and personal for a few years now, but I was recently shown a feature in the Docker file that I wasn’t aware of.
A bit of background - I was building a Go application that would sit alongside Squid to perform some updates. Although I was building the appliction with GOOS
configured, the created Docker image would only work on my Macbook. Setting GOARCH
also fixed it, but this is more interesting and certainly more portable solution.
Multi Stage
Since version 17.06.1-ee-1, Docker has added support for multi stage build in the Docker file. This means you can build your code in on container and make it available to other image builds. The obvious benefit of this for my case is that the target base image can be used as the build env too.
An Example
I wanted to provide a very basic example, so I thought I would create a dummy application that I could build the Dockerfile for.
The Application
The application is going to provide a simple web server that when passed a status code in the path with return a response with that code. This could be potentially useful if you want to test getting various status codes in the wild.
I am writing the application in Go because that is the language I have started using at work - so its all good practice.
HTTP Server
The application creates an HTTP server to listen on port 8080 and return the response with an appropriate status code.
package example
import (
"fmt"
"net/http"
"os"
"strconv"
)
func StartServer(stop chan bool) {
start := make(chan bool, 1)
http.HandleFunc("/", HttpCodeServer)3
go func() {
for {
select {
case <-start:
println(fmt.Sprintf("Starting the service on %s", port))
err := http.ListenAndServe("0.0.0.0:8080", nil)
if err != nil {
println("Restarting the server...")
start <- true
}
case <-stop:
break
}
}
}()
println("Starting the server...")
start <- true
}
func HttpCodeServer(w http.ResponseWriter, r *http.Request) {
statusCode, err := strconv.Atoi(r.URL.Path[1:])
if err != nil {
fmt.Fprintf(w, "Couldn't handle status code %s!", r.URL.Path[1:])
}
w.WriteHeader(statusCode)
}
Main command
The application needs an entry point, this is found in main.go
under cmd/httpcodes
path.
package main
import (
"httpcodes/internal/app/example"
"os"
"os/signal"
)
func main() {
stopServer := make(chan bool, 1)
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt)
go func() {
stopServer <- <-stop == os.Interrupt
}()
httpcodes.StartServer(stopServer)
<-stop
}
This code starts the httpcodes server which will do the listening. A channel is used to stop the server when the application recieves a SIGINT
.
Folder Structure
I have followed the folder structure laid down in the Go project layout guide.
This puts the Dockerfile
into the build/packages
structure.
.
├── build
│ └── package
│ └── httpcodes
│ └── Dockerfile
├── cmd
│ └── httpcodes
│ └── main.go
└── internal
└── app
└── httpcodes
└── server.go
Dockerfile
This was the original point of the post, the Dockerfile
.
FROM golang:alpine AS builder
ENV SRCPATH $GOPATH/src/httpcodes
COPY ./ $SRCPATH
RUN go install httpcodes/cmd/httpcodes
FROM alpine:3.11.3
EXPOSE 8080
RUN mkdir -p /app
COPY --from=builder /go/bin/httpcodes /app/
RUN chmod +x /app/httpcodes
CMD ["./app/httpcodes"]
I have kept it deliberately straight forward to highlight the main point.
At the top I am using golang:alpine
which has all the required packages already installed to build my application. Notice that I mark this AS builder
.
Now, when I create the actually build of the main image, in the COPY
command you can see that the --from=builder
attribute is used. This attribute is telling the second image to grab the /go/bin/httpcodes
binary from the output of the first stage and copy them to the /app
folder.
Building and Running
All that is left now is to build and run. From the root of the project run;
docker build -t httpcodes -f build/package/httpcodes/Dockerfile .
This will build the project and you can now run it with;
docker run -d -p 8080:8080 httpcodes
This command will start the image with port 8080
mapped to the host machine so we can access it. The -d
just runs it in detached mode.
Testing
To test, execute a curl command against localhost:8080/
passing a status code.
for example;
curl -v http://localhost:8080/502
This should return a verbose message saying we got a 502
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to localhost (127.0.0.1) port 8080 (#0)
> GET /502 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.64.1
> Accept: */*
>
< HTTP/1.1 502 Bad Gateway
< Date: Sat, 25 Jan 2020 13:02:31 GMT
< Content-Length: 0
<
* Connection #0 to host localhost left intact
* Closing connection 0