Docker is a modern technology for containerizing software. While packaging to a working version is relatively easy, creating a lightweight Docker image presents a challenge. In this note, I will briefly discuss an example from my university class on software engineering.
The task was to containerize a card game web application, written by students as a group project. The application was created with React.js and used to be run in a development mode (which supports tracing, debugging and so on) via npm start
. The page can be then optimized for deployment via npm build
. The job of a Docker container is to serve this optimized version with a simple http server.
Of course, the easiest way was to both page build and deployment within the Create React App framework. But the optimal way is to stay minimalistic and leverage multi-stage Docker builds with task-tailored sub-images: one to build and another to serve. The image size indeed gets reduced from 500Mb to about 20MB! To build and serve we used, respectively, a Node.js image and a Static Web Server image. The build product of the first step gets copied to the second image which provides a lightweight server, leaving unessential stuff (cached files, development tools ) behind. The implementation is shown below:
# docker build -t brydz:latest .
# docker run -it -p 3000:3000 brydz:latest sh
# stage 1: install dependencies and build the app
FROM node:18-alpine AS pre_build
WORKDIR /brydz
COPY client/package.json ./client/package.json
COPY client/package-lock.json ./client/package-lock.json
RUN npm install --prefix client
COPY client/ ./client/
ENV PUBLIC_URL=“.”
RUN npm run build --prefix client
# stage 2: move to a clean prod environment
FROM joseluisq/static-web-server:2-alpine AS final
WORKDIR /brydz
COPY --from=pre_build /brydz/client/build ./build
EXPOSE 3000
CMD ["static-web-server", "--port=3000", "--root=build", "--log-level=trace"]