Common Dockefile recipes
Development
Considerations for development Dockerfile
s:
- Use a "full" node image, as convenience is more important than saving space in development
- Install
vim
andranger
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
ordebian-slim
and only install the absolutely necessary OS dependencies - Be as explicit as possible in image dependencies (eg.
node:12.22.1-alpine
, notnode: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
asARG
s and then useENV
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).
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.
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"]