Search code examples
dockergocontainers

"localhost didn't send any data" when using Golang + Gin + Docker


I've created a simple API following a youtube tutorial that works perfectly locally. Once I containerise the app and run the container, I can't access the API at http://localhost:8080. I'm guessing it has something to do with the port settings I'm using in the dockerfile, but I'm not sure.

main.go file:

package main

import (
    "net/http"
    "github.com/gin-gonic/gin"
    "errors"
)

type phone struct{
    ID       string `json:"id"`
    Model    string `json:"model"`
    Year     string `json:"year"`
    Quantity int    `json:"quantity"`
}

var phones = []phone{
    {ID: "1", Model: "iPhone 11", Year: "2019", Quantity: 4},
    {ID: "2", Model: "iPhone 6", Year: "2014", Quantity: 9},
    {ID: "3", Model: "iPhone X", Year: "2017", Quantity: 2},
}

func phoneById(c *gin.Context) {
    id := c.Param("id")
    phone, err := getPhoneById(id)

    if err != nil {
        c.IndentedJSON(http.StatusNotFound, gin.H{"message": "Phone not found."})
        return
    }

    c.IndentedJSON(http.StatusOK, phone)
}

func checkoutPhone(c *gin.Context) {
    id, ok := c.GetQuery("id")

    if !ok {
        c.IndentedJSON(http.StatusBadRequest, gin.H{"Message": "Missing id query paramater"})
        return
    }

    phone, err := getPhoneById(id)

    if err != nil {
        c.IndentedJSON(http.StatusBadRequest, gin.H{"Message": "Phone not found"})
        return
    }

    if phone.Quantity <= 0 {
        c.IndentedJSON(http.StatusBadRequest, gin.H{"Message": "Phone not available."})
        return
    }

    phone.Quantity -= 1
    c.IndentedJSON(http.StatusOK, phone)
}

func returnPhone(c *gin.Context) {
    id, ok := c.GetQuery("id")

    if !ok {
        c.IndentedJSON(http.StatusBadRequest, gin.H{"Message": "Missing id query paramater"})
        return
    }

    phone, err := getPhoneById(id)

    if err != nil {
        c.IndentedJSON(http.StatusBadRequest, gin.H{"Message": "Phone not found"})
        return
    }

    if phone.Quantity <= 0 {
        c.IndentedJSON(http.StatusBadRequest, gin.H{"Message": "Phone not available."})
        return
    }

    phone.Quantity += 1
    c.IndentedJSON(http.StatusOK, phone)
}

func getPhoneById(id string) (*phone, error) {
    for i, p := range phones {
        if p.ID == id {
            return &phones[i], nil
        }
    }

    return nil, errors.New("Phone not found.")
}


func getPhones(c *gin.Context) {
    c.IndentedJSON(http.StatusOK, phones)
}

func createPhone(c *gin.Context) {
    var newPhone phone

    if err := c.BindJSON(&newPhone); err != nil {
        return 
    }

    phones = append(phones, newPhone)
    c.IndentedJSON(http.StatusCreated, newPhone)
}

func main(){
    router := gin.Default()
    router.GET("/phones", getPhones)
    router.GET("/phones/:id", phoneById)
    router.POST("/phones", createPhone)
    router.PATCH("/checkout", checkoutPhone)
    router.PATCH("/return", returnPhone)
    router.Run("localhost:8080")
}

and my dockerfile:

#The standard golang image contains all of the resources to build
#But is very large.  So build on it, then copy the output to the
#final runtime container
FROM golang:latest AS buildContainer
WORKDIR /go/src/app

COPY . .

#flags: -s -w to remove symbol table and debug info
#CGO_ENALBED=0 is required for the code to run properly when copied alpine
RUN CGO_ENABLED=0 GOOS=linux go build -v -mod mod -ldflags "-s -w" -o restapi .

#Now build the runtime container, just a stripped down linux and copy the
#binary to it.
FROM alpine:latest
WORKDIR /app
COPY --from=buildContainer /go/src/app/restapi .

ENV GIN_MODE release

ENV HOST 0.0.0.0
ENV PORT 8080
EXPOSE 8080

CMD ["./restapi"]

I've tried different dockerfiles found on Google, and tried creating my own from scratch.


Solution

  • You need to bind to the public network interface inside the container. Because each container is its own host, and when you bind to the loopback interface inside, it will not be accessible to the outside world.

    router.Run("0.0.0.0:8080")
    

    Additionally, make sure you publish this port when running the container.

    docker run --publish 8080:8080 myapp
    

    You actually indicate the right intend with your environment variables, but they are not used in your code.

    ENV HOST 0.0.0.0
    ENV PORT 8080
    

    You can use os.Getenv or os.LookupEnv to get those variables from your code and use them.