Notes on “Docker for Note.js Projects …”
Reference to the course: Docker for Note.js Projects from a Docker Captain
I will update this post while continuing advance through the course.
Last updated: 25 February 2023.
Docker Compose basics
Docker Compose is made of two things: the Compose CLI, and Compose file.
Docker Compose CLI is mostly a substitute for Docker CLI.
It is most useful in development environment.
Docker Compose file is usually named docker-compose.yml:
services: web: image: sample-01 build: . ports: - "3000:3000"
A service in this context is a container based on a single image.
Get inside the container, and inspect things from inside it.
docker compose exec <service name> sh
Node Dockerfile best practice basics
ADDis much more powerfull, but to put something inside our image
COPYis usually enough.
npm config list && npm ciPrint debug info about node/npm environment before installing node dependencies.
npm ci && npm cache clean --forceAlways clear cache after installing dependencies, in order to reduce image size.
CMD node, not
Add node_modules/.bin to PATH
# Dockerfile ENV PATH=/path/to/node_modules/.bin:$PATH
Alpine is small, and security focused. Debian/Ubuntu are small now too (~100MB space savings isn’t significant).
Official node image comes already with a predefined node user. Change user from root to node:
It solves one, major complexity as your complexity grows in your Dockerfiles. That is, how do we take a single Dockerfile, with a single FROM, and single CMD. How do we use that in all the different places.
FROM node:10-slim as prod ENV NODE_ENV=production WORKDIR /app COPY package*.json ./ RUN npm install --only=production \ && npm cache clean --force COPY . . CMD ["node", "./bin/www"] FROM prod as dev ENV NODE_ENV=development RUN npm install --only=development CMD ["node_modules/nodemon/bin/nodemon.js", "./bin/www", "--inspect=0.0.0.0:9229"] FROM dev as test ENV NODE_ENV=development CMD ["npm", "test"]
Note: this example uses
npm install, but it would be better to use
npm ci (ref) that strictly respects the dependencies version listed in the lock file.
npm ci uses the
NODE_ENV variable to determine whether it should install dev dependencies.
target to target a specific stage; eg.
services: my-service: build: context: . target: dev ...
A more complex example:
## Stage 1 (production base) # This gets our prod dependencies installed and out of the way FROM node:10-alpine as base EXPOSE 3000 ENV NODE_ENV=production WORKDIR /opt COPY package*.json ./ # we use npm ci here so only the package-lock.json file is used RUN npm config list \ && npm ci \ && npm cache clean --force ## Stage 2 (development) # we don't COPY in this stage because for dev you'll bind-mount anyway # this saves time when building locally for dev via docker-compose FROM base as dev ENV NODE_ENV=development ENV PATH=/opt/node_modules/.bin:$PATH WORKDIR /opt RUN npm install --only=development WORKDIR /opt/app CMD ["nodemon", "./bin/www", "--inspect=0.0.0.0:9229"] ## Stage 3 (copy in source) # This gets our source code into builder for use in next two stages # It gets its own stage so we don't have to copy twice # this stage starts from the first one and skips the last two FROM base as source WORKDIR /opt/app COPY . . ## Stage 4 (testing) # use this in automated CI # it has prod and dev npm dependencies # In 18.09 or older builder, this will always run # In BuildKit, this will be skipped by default FROM source as test ENV NODE_ENV=development ENV PATH=/opt/node_modules/.bin:$PATH # this copies all dependencies (prod+dev) COPY --from=dev /opt/node_modules /opt/node_modules # run linters as part of build # be sure they are installed with devDependencies RUN eslint . # run unit tests as part of build RUN npm test # run integration testing with docker-compose later CMD ["npm", "run", "int-test"] ## Stage 5 (default, production) # this will run by default if you don't include a target # it has prod-only dependencies # In BuildKit, this is skipped for dev and test stages FROM source as prod CMD ["node", "./bin/www"]
The Twelve-Factor App
Prevent bloat and unneeded files in the image.
.DS_Store .git/ node_modules/ npm-debug docker-compose*.yml
- Don’t use host file paths
- Don’t bind mount databases
# same as # docker container run -p 80:4000 -v $(pwd):/site bretfisher/jekyll-serve services: jekyll: image: bretfisher/jekyll-serve volumes: - .:/site ports: - "80:4000"
docker compose up.
Node Modules in Bind-Mounts
When we bind mount our source in Docker we end up with node_modules from host inside the container. This is not desiderable, especially on MacOS/Windows cause it’s a different architecture than Linux.
There are two ways to avoid this:
npm i on host, run
npm i in compose.
Then when there’s the bind mount to your host, it will then put those Node modules on the host OS.
# Run install command within the container # 'express' is the service name from the compose file docker compose run express npm install
This means that you can’t do Node development natively on your host anymore because those Node modules are designed for Linux.
Move node_modules in image, hide node_modules from host.
FROM node:10.15-slim ENV NODE_ENV=production WORKDIR /node COPY package.json package-lock*.json ./ RUN npm install && npm cache clean --force WORKDIR /node/app COPY . . CMD ["node", "./bin/www"]
node_modules will be installed in
/node folder, the source will be in
version: '2.4' services: express: build: . ports: - 3000:3000 volumes: - .:/node/app - /node/app/node_modules # make the node_modules directory empty environment: - DEBUG=sample-express:*
Run various tools inside the container
# Start a new container and run command/shell docker compose run <service> bash docker compose run <service> npm install
# Run additional command/shell in a currently running container docker compose exec <service> bash docker compose exec <service> npm install
Node Auto Restarts
- Override startup command from docker-compose.yml
- Use nodemon
version: '2.4' services: express: build: . command: /app/node_modules/.bin/nodemon ./bin/www ports: - 3000:3000 volumes: - .:/node/app - /node/app/node_modules # make the node_modules directory empty environment: - DEBUG=sample-express:* - NODE_ENV=development
Startup Order and Dependencies
Multi-container apps need:
- Dependency awareness,
- Name resolution (DNS)
- Connection failure handling,
depends_on allow to have an healthcheck.
version: '2.4' services: frontend: image: nginx depends_on: api: # this requires a compose file version => 2.3 and < 3.0, # and to define a healthcheck test for the `api` service condition: service_healthy api: image: node:alpine healthcheck: test: curl -f http://127.0.0.1