I am trying to containerize a Go app that serves static files on port 8000. I have looked at other posts on this topic and many seem to say to use router.Run("0.0.0.0:8000")
or router.Run(":8000")
. I have tried both but still no success. My main.go looks like this:
package main
// required packages
import (
"fmt"
"log"
"os"
"github.com/gin-gonic/gin"
"github.com/gin-contrib/cors
"net/http"
)
func main() {
// start the server
serveApplication()
}
func serveApplication() {
corsConfig := cors.Config {
AllowOrigins: []string{"*"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
AllowCredentials: true,
}
router := gin.Default()
router.Use(cors.New(corsConfig))
router.StaticFS("/static", http.Dir("./frontend/build/static"))
router.StaticFile("/", "./frontend/build/index.html")
router.Run(":8000")
fmt.Println("Server running on port 8000")
}
And the following Dockerfile:
FROM node:16-alpine3.11 as build-node
WORKDIR /workdir
COPY frontend/ .
RUN yarn install
RUN yarn build
COPY .env /workdir/
FROM golang:1.21-rc-alpine3.18 as build-go
#FROM golang:1.17rc2-alpine3.14 as build-go
ENV GOPATH ""
RUN go env -w GOPROXY=direct
RUN apk add git
ADD go.mod go.sum ./
RUN go mod download
ADD . .
COPY --from=build-node /workdir/build ./frontend/build
RUN go build -o /main
FROM alpine:3.13
COPY --from=build-go /main /main
COPY --from=build-node /workdir/.env .env
EXPOSE 8000
ENTRYPOINT [ "/main" ]
My folder structure looks like this;
portal
- frontend (here the react app is stored)
- backend (all my backend logic)
- Dockerfile
- main.go
- go.mod
When I run it locally with go run main.go
the frontend is served correctly on port 8000 and loading http://localhost:8000 works fine. When I build the docker image with docker build -t portal .
and then run it with docker run -p 8000:8000 --name portal portal
I can see in the terminal that the server starts and says it is running on port 8000, but I always get a 404 page not found error.
I have tried using router.run("0.0.0.0:8000")
, router.run("localhost:8000")
or docker run --network host --name portal portal
.
Is there anything I am missing? Am I copying the frontend build to the wrong place?
The only things in your final image are the things you COPY
after the last FROM
line; that is, the main
binary and the .env
file. You're trying to serve files out of ./frontend/...
but that's not in the final image. Just moving the relevant COPY
line to the final stage should work.
FROM alpine:3.13
COPY --from=build-go /main /main
COPY --from=build-node /workdir/.env .env
COPY --from=build-node /workdir/build ./frontend/build # <-- move from above
...
Conversely, since you're not using something like the embed
package to directly embed the built frontend code into the binary, you don't need it during the (Go) build stage.
It may also work to use embed
, and not rearrange your Dockerfile. This would look roughly like
//go:embed frontend/build/*
var content embed.FS
router.StaticFS("/static", http.FS(fs.Sub(content, "frontend/build/static"))
router.StaticFileFS("/", "frontend/build/index.html", http.FS(content))
With this setup the frontend does need to be part of your Go build step, but now it is fully contained in the binary and does not need to be separately copied into the final image.