1

I'm having difficulties setting up a golang dockerfile for a personal project.

project structure is:

Project
|
+-- log.go (contains main)
|
+-- go.mod
|      
+-- hash
   |  
   +-- hash.go

The app prints a random hash every 5 seconds and appends a timestamp to it.

File contents:

log.go

  package main

  import (
      "fmt"
      "github.com/postelniql/logger-output/hash"
      "time"
  )

  func log() string {
      dt := time.Now()
      hash := hash.NewSHA1Hash()
      return dt.Format("01-02-2006T15:04:05.000Z") + ": " + hash
  }

  func main() {
      fmt.Println(log())

      tick := time.Tick(5000 * time.Millisecond)
      for range tick {
          fmt.Println(log())
      }
  }

go.mod:

  module github.com/postelniql/logger-output

  go 1.19

hash.go:

  package hash

  import (
      "crypto/sha1"
      "fmt"
      "math/rand"
      "time"
  )

  func init() {
      rand.Seed(time.Now().UnixNano())
  }

  func NewSHA1Hash(n ...int) string {
      noRandomCharacters := 32

      if len(n) > 0 {
          noRandomCharacters = n[0]
      }

      randString := randomString(noRandomCharacters)

      hash := sha1.New()
      hash.Write([]byte(randString))
      bs := hash.Sum(nil)

      return fmt.Sprintf("%x", bs)
  }

  var characterRunes = 
  []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789")

  // RandomString generates a random string of n length
  func randomString(n int) string {
      b := make([]rune, n)
      for i := range b {
          b[i] = characterRunes[rand.Intn(len(characterRunes))]
      }
      return string(b)
  }

I scraped together the following Dockerfile:

  FROM golang:1.19-alpine
  WORKDIR /app
  COPY go.mod ./
  RUN apk add git
  RUN go get github.com/postelniql/logger-output/hash
  COPY *.go ./
  RUN go build -o /logger-output-app
  EXPOSE 8080
  CMD [ "/logger-output-app" ]

However I keep getting this error (and similar sort of errors):

   ------
    > [6/8] RUN go get github.com/postelniql/logger-output/hash:
    #10 2.105 go: github.com/postelniql/logger-output/hash: no matching versions for 
    query "upgrade"
   ------
   executor failed running [/bin/sh -c go get github.com/postelniql/logger- 
   output/hash]: exit code: 1

I've searched the web for hours trying to fix this, I genuinely don't understand what's wrong with it. I suspect I'm doing something wrong when it comes to dependency management in the Dockerfile.

I mention I'm a noob in go and am coding this as part of my learning process.

Please help me write a dockerfile that builds and runs.

Thanks!

someposte
  • 25
  • 6

2 Answers2

2

This should work, explanation below.

#build stage
FROM golang:alpine AS builder

RUN apk add --no-cache git
WORKDIR /go/src/app
COPY . .

RUN go get -d -v ./...
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /go/bin/app -v .

#final stage
FROM alpine:latest

RUN addgroup -S app && adduser -S app -G app
COPY --from=builder --chown=app /go/bin/app /app
USER app

ENTRYPOINT ["/app"] 

Place the Dockerfile in your project so that you don't have to clone the project inside (makes no sense).

Use 2-stage build to have a clean final image.

Use a different user than root to run the final binary.

Don't expose any port since your app is not listening on any port.

Use ENTRYPOINT instead of CMD. This way you can later pass arguments on the docker run command line.

Mihai
  • 9,526
  • 2
  • 18
  • 40
  • Hey Mihai, thanks a lot for your answer! Running the dockerfile as it is, I get this error `=> ERROR [builder 6/6] RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /go/bin/app -v ./... ------ > [builder 6/6] RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /go/bin/app -v ./...: #13 0.286 go: cannot write multiple packages to non-directory /go/bin/app ------ executor failed running [/bin/sh -c CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /go/bin/app -v ./...]: exit code: 1` – someposte Sep 25 '22 at 09:16
  • It builds if I substitute `go/bin` for `go/app` however when I run it with `docker run --user=app:app logger-output-app:latest`, I get the following error `docker: Error response from daemon: failed to create shim task: OCI runtime create failed: runc create failed: unable to start container process: exec: "/app": permission denied: unknown. ERRO[0000] error waiting for container: context canceled` – someposte Sep 25 '22 at 09:18
  • 1
    Use `.` as the path at the end of the `go build` line (whichever single Go package contains your `main()` function). I might change the final `COPY` to `COPY --from=builder /go/bin/app /usr/local/bin/app`, without `chown`ing it (leave the binary owned by root, world-executable, and unmodifiable) and in a standard `$PATH` directory. – David Maze Sep 25 '22 at 11:12
  • @DavidMaze that was it!! Thank you so much! :) Thanks as well for your breakdown of the Dockerfile below. It's great to be able to compare them and there's a lot to learn from both! – someposte Sep 25 '22 at 14:38
1

You don't need to go get github.com/postelniql/logger-output/hash: this is part of your local source tree and you have it locally. You do need to make sure you COPY it into your image.

The changes in your Dockerfile aren't complicated, but your Dockerfile isn't that large to start with:

FROM golang:1.19-alpine
WORKDIR /app
COPY go.mod go.sum ./    # add `go.sum`
RUN go mod download      # add: downloads external dependencies
# RUN apk add git        # delete
# RUN go get github.com/postelniql/logger-output/hash  # delete
COPY *.go ./
COPY hash/ hash/         # add
RUN go build -o /logger-output-app
EXPOSE 8080
CMD [ "/logger-output-app" ]

The Dockerfile in @Mihai's answer should work too, since it also deletes the go get your-own-application line. The multi-stage build setup deletes the Go toolchain from the final build so you get a much smaller image out, but it's also more complex and a little harder to debug.

David Maze
  • 130,717
  • 29
  • 175
  • 215
  • thanks for point to the fix in my solution. I don't agree with running the binary as root, probably comes from running go in production for too long. Also I discourage any debugging in the container: any issues should be solved outside the container where all the tools are available. I guess some things are good enough for SO in the end. – Mihai Sep 25 '22 at 14:33
  • As mentioned above, thanks a lot for your comment. I was able to get things running with both versions of Dockerfiles, and learned a bunch in the process too! – someposte Sep 25 '22 at 14:39
  • Could any of you (@Mihai, David or you, reader) please recommend some ways to approach debugging dockerfiles / containerisation builds? All I've done so far is comment the progress reached in the dockerfile itself. Are there any ways you do it to get a dockerfile working first try? :) – someposte Sep 25 '22 at 14:43
  • @Mihai Running as not-root is definitely a best practice, and I agree with reproducing and debugging issues outside the container. (My production Go images tend to be built `FROM scratch`.) – David Maze Sep 25 '22 at 19:25
  • @someposte Rebuilding images shouldn't be that expensive, so it can work to build up the Docker a line at a time and `docker run --rm the-partial-image /bin/sh` to try to understand where you're at. [Can I run an intermediate layer of a Docker image?](https://stackoverflow.com/questions/42602731/can-i-run-an-intermediate-layer-of-a-docker-image) is a useful technique if you disable the BuildKit engine. There are also common recipes for most languages; most Go Dockerfiles look more or less like what you're seeing in these answers. – David Maze Sep 25 '22 at 19:43
  • @DavidMaze thanks a bunch! I'll give these a go next time I'm in trouble :) – someposte Sep 26 '22 at 19:10