Developing NodeJS applications can be challenging, dockerizing them for production use adds another layer of complexity as only the application’s functional scope should be provided by the production image.
docker-build-nodejs
helps in building a production image that is reduced to the application’s functional scope.
In order to do this, docker-build-nodejs
relies on:
-
esbuild for the tree shaking
-
Node Single Executable Applications to bundle the javascript file into an executable
-
Google’s distroless docker images to ensure only the NodeJS based executable is available in the production image
Note
|
The following sections detail how these different Docker images can be used to embed a NodeJS application. Particular details are given about But you will still have to read these sections in order to get a better understanding of how to use the images. |
This repository mainly builds three Docker images.
The docker-build-nodejs-devenv
image is provided to ease the building of a Docker image tailored for development.
The two other images are designed as a mean to build production Docker images embedding a NodeJS based application:
-
docker-build-nodejs-builder
is in charge of building a Linux executable from the Javascript files -
docker-build-nodejs-runtime
is in charge of providing the minimal base environment for the Linux executable to run on
The goal is to ensure the usage of predictable versions of commands such as npm
or node
.
To do this, a development Docker container providing the necessary versions of these tools is started.
One key point is that all these tools need to be started by a user with the same user and group identifiers as the ones used by the developer on its host.
Creating a Dockerfile
You will have to provide a Dockerfile
with at least the following line:
FROM ghcr.io/r2d2bzh/docker-build-nodejs-devenv:dev
Important
|
Replace the tag dev with a tag delivered on the r2d2bzh/docker-build-nodejs project.
|
Creating a docker-compose file
This docker-compose.yml
file will help to easily build and start the development container.
It should contain something close to the following:
dev:
build:
context: dev
args:
USER: ${LOGNAME}
UID: ${UID}
GID: ${GID}
volumes:
- .:/home/user/dev
Tip
|
Most of the time the Hence: .profile
export UID
export GID=$(id -g) |
To start the development environment simply issue the following command:
docker-compose up -d dev
You can now use any NodeJS related command within the container as you would do it directly in the project by prefixing it with docker-compose exec dev
, for instance:
docker-compose exec dev npm install
Tip
|
You can define a shell alias to avoid typing the whole command each time (alias ded=docker-compose exec dev to be able to type ded npm install ) but beware that you will lose the completion provided for the docker-compose command.
|
Embedding a NodeJS application is necessary to provide a production Docker image which guarantees that no other command than the application itself can be started in a container based on this image. In particular, no shell is available within such a container.
Creating a Dockerfile
You will have to provide a Dockerfile
with at least the following two lines:
FROM ghcr.io/r2d2bzh/docker-build-nodejs-builder:dev as builder
COPY --chown=user . /project
RUN /build.sh
FROM ghcr.io/r2d2bzh/docker-build-nodejs-runtime:dev
COPY --chown=user --from=builder /tmp/service /service
ENTRYPOINT [ "/service" ]
COPY --chown=user ./resources /resources
Important
|
Replace the tag dev with a tag delivered on the r2d2bzh/docker-build-nodejs project.
|
This Dockerfile
should be located at the root of your NodeJS project or at least in a folder containing all the source code of the NodeJS application.
You can optionally add additional Dockerfile
commands, it is at least recommended to document the port the NodeJS application is listening on (if the NodeJS application offers such a port):
FROM ghcr.io/r2d2bzh/docker-build-nodejs-builder:dev as builder
COPY --chown=user . /project
RUN /build.sh
FROM ghcr.io/r2d2bzh/docker-build-nodejs-runtime:dev
COPY --chown=user --from=builder /tmp/service /service
ENTRYPOINT [ "/service" ]
COPY --chown=user ./resources /resources
EXPOSE 8080
Warning
|
Do not modify the entry point of the Docker image with ENTRYPOINT as the default entry point is already the application executable.
|
Warning
|
Use the same tag for both FROM instructions as both builder and runtime images are closely related.
|
By default esbuild will be passed index.js
as the main module of the application to embed.
If the application main module is not index.js
, simply set the main
build argument to the right path of the main module:
test-simple:
build:
context: test/simple
args:
main: simple.js
Once the Dockerfile
is available, you can at least operate a test build with the following command:
cd <Dockerfile folder>
docker build -t <target> .
Once the build succeeds, the image can be tested:
docker run --rm -it <target>
Tip
|
Do not forget to publish the port your application is listening on to operate some requests from your development platform. |
To avoid repeating on and on the same docker build
command with all its arguments, you might want to create a docker-compose.yml
file detailing this data, i.e.:
services:
production:
image: <target>
build:
context: <Dockerfile folder>
Once the compose file is available, simply issue the command docker-compose build production
to build the image.
You can also push this new image to a registry with docker-compose push production
as long as the image tag refers to a location on this registry.
Automatic native modules bundling might sometimes fail for various reasons. The main reason is most of the time because the files to bundle cannot be inferred by esbuild.
In these particular cases, follow the instructions provided in the console where the build was operated:
console.warn('/!\\ Some node modules were automatically externalized');
console.warn('If one of these modules can still NOT be loaded:');
console.warn(' - add the module name in your package.json file under { esbuildOptions: { external: [...] } }');
console.warn(' - add the module COPY line provided in the following list at the end of your Dockerfile');
The console then displays the list of externalized modules and the Dockerfile COPY
lines to use.
The test/sharp
test case of this repository follows this advice for sharp
:
{
...
"esbuildOptions": {
"external": ["sharp"]
},
...
}
FROM ghcr.io/r2d2bzh/docker-build-nodejs-builder:dev as builder
COPY --chown=user . /project
RUN /build.sh
FROM ghcr.io/r2d2bzh/docker-build-nodejs-runtime:dev
COPY --chown=user --from=builder /tmp/service /service
COPY --from=builder /project/node_modules/ ./node_modules/
ENTRYPOINT [ "/service" ]