In the previous post, we build a REST API server with Express and TypeScript. In this post, we will dockerize the sever.
Why Docker.
Docker helps organizations to ship and develop applications better and faster. It will be easy to set up the development environment on any new machine with docker as it abstracts out lots of complexity of setting up dependencies and environment. Docker also isolates the project from other projects in the same machine so the developer can run multiple projects without having any conflict with the required dependencies.
Docker makes it easy to configure and setup dependencies and environments for the application. As most of the companies have dedicated teams to do setup and manage infrastructure, Docker gives more power to developers to configure without depending on other teams to do the setup.
Write Dockerfile.
To Dockerize the server, we need to create a Dockerfile
. A Dockerfile is just a list of instructions to create a docker image. Read more about Dockerfile here
Each line in the Dockerfile is a command and create a new image layer of its own. Docker caches the images during the build, so every rebuild will only create the new layer which got changed from the last build. Here the order of commands is very significant as it helps to reduce the build time.
Let's start writing Dockerfile for the server. Here we are taking node:12
as the base image for the server docker image. Explore dockerhub for more node image version. Here we are copying the package.json
and doing npm install
first, then copying the other files. Docker will cache the images of these two steps during the build and reuse them later as they change less frequently. Here we will be running the development server with the docker image, so we need to give npm run dev
as the executing command.
Dockerfile
FROM node:12
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
EXPOSE 8000
CMD ["npm", "run", "dev"]
We need to add .dockerignore
to tell docker build to ignore some files during the COPY
Command.
.dockerignore
node_modules
npm-debug.log
After creating the Dockerfile, we need to run the docker build
to create a docker image from the Dockerfile. Here we are naming the docker image as express-ts
docker build -t express-ts .
We can verify the docker image by running the docker images
command. Here we can see the name, size, and tag of the docker images.
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
express-ts latest d0ce1e38958b 2 minutes ago 1.11GB
We can run the docker image with the docker run
command. Here we can mapping the system port 8000 to docker container port 8000. We can verify if the server is running or not by visiting http://localhost:8000/ping
docker run -p 8000:8000 express-ts
Add Docker Compose
The development server is running fine inside docker, but now we need to run the docker build
command every time after making any changes to the source files to update the changes during development because the nodemon inside the docker container cannot watch the src
folder on the local machine. We need to mount the local src
folder to the docker container folder, so every time we make any change inside the src
folder, nodemon restarts the development server inside the docker container.
We will add the docker-compose.yml
file to the root of the project to mount the local src
folder. Read more about docker-compose here
docker-compose.yml
version: "3"
services:
app:
build:
context: .
dockerfile: Dockerfile
volumes:
- ./src:/app/src
ports:
- "8000:8000"
We need to run the command docker-compose up
to start the server. Now the server is running in development mode with auto-restart on code changes. We can verify the server is restarting on code changes by making any code change in the TypeScript files.
docker-compose up
The docker setup for the development server is completed. Let's rename the Dockerfile as Dockerfile.dev
and update the docker-compose.yaml file. We will use the Dockerfile
for the production image, which we are going to set up in the next section.
mv Dockerfile Dockerfile.dev
docker-compose.yml
version: "3"
services:
app:
build:
context: .
dockerfile: Dockerfile.dev
volumes:
- ./src:/app/src
ports:
- "8000:8000"
Add Production Dockerfile
Let's start building a docker image for the production server. We need to create a new Dockerfile and add the following commands. Here after copying the files, we need to build the JavaSript files and execute the npm start
command.
FROM node:12
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
EXPOSE 8000
CMD ["node", "start"]
After running the docker build
command, we can see the docker image is created for the production server.
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
express-ts latest d0ce1e38958b 2 minutes ago 1.11GB
Here the image size is 1.11GB
, which is not optimized. Let's optimize the docker image and reduce the size.
First, instead of taking node:12
as the base image, we will be taking its alpine variant. Alpine Linux is very lightweight. Read more about alpine-docker here.
FROM node:12-alpine
Let's build the docker image with the updated Dockerfile. Here we are tagging the docker image as alpine
so we can compare the image size with the previous build.
docker build -t express-ts/alpine .
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
express-ts alpine 2b06fcba880e 46 seconds ago 280MB
express-ts latest d0ce1e38958b 2 minutes ago 1.11GB
After running the docker images
command we can see the difference in the sizes of docker images. The docker image is much leaner than the previous build.
There are still some issues with our docker image as development dependencies are there in production build and TypeScript code is there, which is not required while running the server in production. So let's optimize the docker image further with a multi-stage build.
Here we create two stages, one for building the server and the other for running the server. In the builder stage, we generate Javascript code from the Typescript files. Then in the server stage, we copy the generated files from the builder stage to the server stage. In the Server stage, we need only production dependencies, that's why we will pass the --production
flag to the npm install
command.
FROM node:12-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM node:12-alpine AS server
WORKDIR /app
COPY package* ./
RUN npm install --production
COPY --from=builder ./app/public ./public
COPY --from=builder ./app/build ./build
EXPOSE 8000
CMD ["npm", "start"]
Let's build the docker image with the updated multi-staged Dockerfile. Here we are tagging the docker image as ms
so we can compare the image sizes with the previous builds.
docker build -t express-ts/ms .
docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
express-ts alpine 2b06fcba880e 46 seconds ago 280MB
express-ts latest d0ce1e38958b 2 minutes ago 1.11GB
express-ts ms 26b67bfe45b0 9 minutes ago 194MB
After running the docker images
command we can see the difference in the sizes of docker images. The multi staged image is the leanest among all images.
We have dockerized the development and production version of the Express and TypeScript REST API server.
All the source code for this tutorial is available on GitHub.
Top comments (0)