Tobias S
Tobias S

Reputation: 330

React client and Golang server in same Dockerfile

I've build a React client application supported with a API written in Golang. I would like to use Docker to run these both apps using docker run.

I have the following project structure:

zid
    |
    |-web/ (my react folder)
    main.go
    Dockerfile
    |

My goal is to run the main.go file in the zid folder and start the webapplication in the zid/web folder. The main.go file starts a API using Gin Gonic that will listen and serve on port 10000.

So I've tried the following:

# Build the Go API
FROM golang:latest as go_builder
RUN mkdir /zid
WORKDIR /zid
COPY . /zid
RUN GOOS=linux GOARCH=amd64 go build -a -ldflags "-linkmode external -extldflags '-static' -s -w" -o /go/bin/zid

# Build the React application
FROM node:alpine as node_builder
COPY --from=go_builder /zid/web ./
RUN npm install
RUN npm run build

# Final stage build, this will be the container with Go and React
FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY --from=go_builder /go/bin/zid /go/zid
COPY --from=go_builder /zid/ca /go/ca
COPY --from=node_builder /build ./web
EXPOSE 3000
WORKDIR /go
CMD ./zid

Next I did the following:

  1. Build it with docker build -t zid . (no errors)
  2. Run it with docker run -p 3000:3000 --rm zid

When I run this, it will startup the API, but when I go to http://localhost:3000/ then I get a Page does not work ERR: ERR_EMPTY_RESPONSE.

So the API starts up, but the npm build doens't. I am not sure what I am doing wrong, because the Docker container both contains the correct folders (go and web).

docker container

As you can see in the image it's all there I believe. What am I missing?

EDIT:

I am using the (*gin.Engine).Run() function to set the listen and serve on port 10000. In my local build my React application is sending request to localhost:10000. I always simply used npm start on the side of my React app (localhost:3000). My goal is to do the same but then all in one Dockerfile.

I am still a little unsure if I should EXPOSE ports 10000 & 3000 in my Dockerfile.

My HandleRequest function:

//Start the router and listen/serve.
func HandleRequests() {
    router := SetupRouter()
    router.Run(":10000")
}

My SetupRouter function:

//Setup the gin router
func SetupRouter() *gin.Engine {
    router := gin.Default()
    router.Use(CORSMiddleware())

    router.POST("/auth/login", login)
    router.POST("/component/deploy", deployComponent)
    router.POST("/project/create", createProject)
    router.POST("/diagram/create", createDiagram)
    router.PATCH("/diagram/update", updateDiagram)
    router.DELETE("/diagram/delete/:id", deleteDiagram)
    router.GET("/diagram/:id", getDiagram)
    router.GET("/project/list", getProjectsByUsername)
    router.GET("/project/:id", getProject)
    router.GET("/project/diagrams/:id", getDiagramsOfProject)
    router.DELETE("/project/delete/:id", deleteProject)
    router.GET("/application/list", applicationList)
    router.GET("/instance/status/:id", getInstanceStatus)
    router.GET("/user", getUser)

    return router
}

Btw I just want to use the Docker container for Development and learning purpose only.

Upvotes: 1

Views: 2130

Answers (2)

Tobias S
Tobias S

Reputation: 330

I've found a solution! I've created a script on basis of multi-service container and then a run this script in my Dockerfile.

my script (start.sh):

#!/bin/sh

# Start the first process
./zid &
ZID_PID=$!

# Start the second process
cd /web 
npm start &
WEB_PID=$!

# Naive check runs checks once a minute to see if either of the processes exited.
# This illustrates part of the heavy lifting you need to do if you want to run
# more than one service in a container. The container exits with an error
# if it detects that either of the processes has exited.
# Otherwise it loops forever, waking up every 60 seconds

while sleep 60; do
  ps -fp $ZID_PID 
  ZID_PROCESS_STATUS=$?
  if [ $ZID_PROCESS_STATUS -ne 0 ]; then
    echo "ZID process has already exited."
    exit 1
  fi
  
  ps -fp $WEB_PID 
  WEB_PROCESS_STATUS=$?
  if [ $WEB_PROCESS_STATUS -ne 0 ]; then
    echo "WEB process has already exited."
    exit 1
  fi
done

Here I first start my go executable and then I do a npm start

In my Dockerfile I do the following:

# Build the Go API
FROM golang:latest as go_builder
RUN mkdir /zid
WORKDIR /zid
COPY . /zid
RUN GOOS=linux GOARCH=amd64 go build -a -ldflags "-linkmode external -extldflags '-static' -s -w" -o /go/bin/zid

# Build the React application
FROM node:alpine as node_builder
COPY --from=go_builder /zid/web ./web
WORKDIR /web
RUN npm install

# Final stage build, this will be the container with Go and React
FROM node:alpine
RUN apk --no-cache add ca-certificates procps 
COPY --from=go_builder /go/bin/zid /go/zid
COPY --from=go_builder /zid/static /go/static 
COPY --from=go_builder /zid/ca /go/ca
COPY --from=node_builder /web /web
COPY --from=go_builder /zid/start.sh /go/start.sh
RUN chmod +x /go/start.sh
EXPOSE 3000 10000
WORKDIR /go
CMD ./start.sh

Here I am creating a Go executable, copy and npm install my /web folder and in de final stage build I start my ./start.sh script.

This will start my Golang application and the React development server. I hope it helps for others.

Upvotes: 1

colm.anseo
colm.anseo

Reputation: 22147

I've used the following multi-stage Docker build to create:

  • static VueJS UI HTML assets
  • compiled Go API http server (serving the above HTML assets)

Note: both Go and VueJS source is download from one git repo - but you could just as easily modify this to copy the two code-bases from local development directories.


#
# go build
#
FROM golang:1.16.5 AS go-build

#
# here we pull pkg source directly from git (and all it's dependencies)
#
RUN     go get  github.com/me/vue-go/rest
WORKDIR /go/src/github.com/me/vue-go/rest
RUN     CGO_ENABLED=0 go build

#
# node build
#
FROM node:13.12.0 AS node-build

WORKDIR /app/vue-go

COPY --from=go-build go/src/github.com/me/vue-go/vue-go ./

# produces static html 'dist' here:
#
#       /app/vue-go/dist
#
RUN npm i && npm run build

#
# final layer: include just go-binary and static html 'dist' 
#
FROM scratch

COPY --from=go-build \
    /go/src/github.com/me/vue-go/rest/rest \
    /app/vue-go

COPY --from=node-build \
    app/vue-go/dist \
    /app/dist/

CMD ["/app/vue-go"]

I don't use Gin - but to use native net/http fileserver serving APIs and static HTML assets, use something like:

h := http.NewServeMux()

// serve static HTML directory:
if conf.StaticDir != "" {
    log.Printf("serving on '/' static files from %q", conf.StaticDir)
    h.Handle(
        "/",
        http.StripPrefix(
            "/",
            http.FileServer(
                http.Dir(conf.StaticDir), // e.g. "../vue-go/dist"  vue.js's html/css/js build directory
            ),
        ),
    )
}


// handle API route(s)
h.Handle("/users",
    authHandler(
        http.HandlerFunc(handleUsers),
    ),
)

and start the service:

s := &http.Server{
    Addr:    ":3000", // external-facing IP/port
    Handler: h,
}

log.Fatal(s.ListenAndServe())

then to build & run:

docker build -t zid .
docker run -p 3000:3000  --rm zid

Upvotes: 1

Related Questions