Skip to main content

Common Dockefile recipes

Development

Considerations for development Dockerfiles:

  • Use a "full" node image, as convenience is more important than saving space in development
  • Install vim and ranger so that you can conveniently navigate the container and debug as needed
  • Install all dependencies, including devDependencies

If your project has a single package.json file for both client and server

Do not define ENTRYPOINT and CMD, as you will use the same image for two very different sets of commands (starting webpack-dev-server and starting the express server). Handle these in your docker-compose file.

FROM node:12

RUN apt-get update \
&& apt-get upgrade -y \
&& apt-get install -y ranger vim

WORKDIR /home/node/app

COPY package.json .
COPY yarn.lock .

RUN chown -R node:node .
USER node

RUN yarn install

COPY --chown=node:node . .

Production

When building production images, we want to keep them with as small a footprint as possible.

  • Use small images like alpine or debian-slim and only install the absolutely necessary OS dependencies
  • Be as explicit as possible in image dependencies (eg. node:12.22.1-alpine, not node:12-alpine). The goal here is predictability in the production environment.
  • Do not install devDependencies
  • Only COPY in relevant files (eg. do not copy test files, storybook files etc.). This can also be controlled with .dockerignore files (see here)

Client

Consideration for client production builds:

  • Build the bundle in an image first, then copy it over to a very minimal image that simply serves the static bundle. The final image won't have any dependencies installed, or any of our code copied in. It will only have the static production bundle.
  • As the bundle is static in production you will need to define the environemnt variables that need to exist in the bundle at build time. The way to do this is to pass them in to the Dockerfile as ARGs and then use ENV to set their values for the build. The variables shown here are just a sample. Add any variables that are relevant to your project.
#################################################
# BUILD
#################################################

FROM node:12.22.1-alpine as build_client

# set up

RUN apk add --no-cache git python make g++ bash
WORKDIR /home/node/app

# install dependencies

COPY package.json .
COPY yarn.lock .

RUN yarn install --frozen-lockfile --production=true

# grab arg values

ARG server_protocol
ARG server_host
ARG server_port

# create env values

ENV NODE_ENV "production"

ENV SERVER_PROTOCOL $server_protocol
ENV SERVER_HOST $server_host
ENV SERVER_PORT $server_port

# build bundle

COPY . .
RUN yarn coko-client-build

#################################################
# CLIENT
#################################################

FROM nginx:1.19.9 as client
COPY --from=build_client /home/node/app/nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=build_client /home/node/app/_build /usr/share/nginx/html

When you start the image nginx will serve the bundle in port 80 inside the container. All you'll need to do is map port 80 to whatever port you need on the host machine (eg. via the docker cli or with a docker-compose file).

tip

nginx needs a little bit of setup to work with single-pageapplications. That is why we copy in an nginx.conf file. For a sample of what this config file could look like, see the nginx recipes page.

tip

nginx is a common tool to use for serving our bundle, but it is not necessary. Feel free to replace it with another tool (eg. http-server) if you prefer.

Server

For the production server, we'll follow a similar to the client two-step solution. This could potentially be easily done with a single image, but it's probably best that your final image does not include compilers like g++.

Two-step setup

#################################################
# BUILD
#################################################

FROM node:12.22.1-alpine as build_server

# set up

RUN apk add --no-cache git python make g++ bash
WORKDIR /home/node/app

COPY package.json .
COPY yarn.lock .

# build the dependencies (python, g++ etc are necessary here)

RUN yarn install --frozen-lockfile --production=true

#################################################
# SERVER
#################################################

FROM node:12.22.1-alpine as server

WORKDIR /home/node/app

RUN chown -R node:node .
USER node

# COPY only necessary folders and files

COPY --chown=node:node ./config ./config
COPY --chown=node:node ./scripts ./scripts
COPY --chown=node:node ./server ./server
COPY --chown=node:node ./static ./static
COPY --chown=node:node ./startServer.js .

# COPY the dependencies in. Now you have the dependencies in an image that
# does not include the compilers and other OS dependencies

COPY --from=build_server /home/node/app/node_modules ./node_modules

ENTRYPOINT ["sh", "scripts/setupProductionServer.sh"]
CMD ["node", "./startServer.js"]

Single image setup

FROM node:12.22.1-alpine

RUN apk add --no-cache git python make g++ bash
WORKDIR /home/node/app

COPY package.json .
COPY yarn.lock .

RUN chown -R node:node .
USER node

RUN yarn install --frozen-lockfile --production=true

COPY --chown=node:node ./config ./config
COPY --chown=node:node ./scripts ./scripts
COPY --chown=node:node ./server ./server
COPY --chown=node:node ./static ./static
COPY --chown=node:node ./startServer.js .

ENTRYPOINT ["sh", "scripts/setupProductionServer.sh"]
CMD ["node", "./startServer.js"]

Serving the client from the server

Although this scenario is best avoided in favour of keeping the client and server separated, there are cases where this is a requirement. The solution is a hybrid approach of the examples above, where we build the client, then copy the bundle into the server image.

The following sample assumes you have a setup with a single package.json file for both client and server.

#################################################
# BUILD
#################################################
FROM node:12.22.1-alpine as build

RUN apk add --no-cache git python make g++

WORKDIR /home/node/app

COPY package.json .
COPY yarn.lock .

# Install production node modules for server use
RUN yarn install --frozen-lockfile --production=true
# Copy to another folder for later use
RUN mv node_modules production_node_modules

# Install development node modules for building webpack bundle
RUN yarn install --frozen-lockfile --production=false

# grab arg values
ARG server_protocol
ARG server_host
ARG server_port

# Makse sure this variable is set for both images
ENV SERVER_SERVE_CLIENT=true

ENV NODE_ENV=production

ENV SERVER_PROTOCOL $server_protocol
ENV SERVER_HOST $server_host
ENV SERVER_PORT $server_port


# build bundle
COPY . .
RUN yarn coko-client-build

#################################################
# SERVER
#################################################
FROM node:12.22.1-alpine as server

WORKDIR /home/node/app

RUN chown -R node:node .
USER node

ENV SERVER_SERVE_CLIENT=true

COPY --chown=node:node ./config ./config
COPY --chown=node:node ./scripts ./scripts
COPY --chown=node:node ./server ./server
COPY --chown=node:node ./startServer.js .

# copy in the client bundle
COPY --from=build /home/node/app/_build/assets ./_build/assets
# copy in the server dependencies
COPY --from=build /home/node/app/production_node_modules ./node_modules

ENTRYPOINT ["sh", "./scripts/setupProdServer.sh"]
CMD ["node", "./startServer.js"]