Docker is a platform to run applications in a container, which is a lightweight version of a virtual machine. The main benefit of using Docker is the ability to deploy an application on any server without having to worry about dependencies or getting into conflicts with other applications on the same server. To build a Docker image, we write instructions in a script named Dockerfile. Once the script is completed, we build the image by running command docker build.

Changes in Codebase

A Node server will run the Tabunganku’s code. To accomplish this, we change the adapter from adapter-auto to adapter-node. Open svelte.config.js and modify the line

1
import adapter from '@sveltejs/adapter-auto';

into

1
import adapter from '@sveltejs/adapter-node';

To enable application termination using ctrl+c or docker stop command, add these lines in src/hooks.server.js:

1
2
process.on('SIGINT', function () { process.exit(); });
process.on('SIGTERM', function () { process.exit(); });

Dockerfile

This is the Dockerfile script used

FROM node:20-alpine as build
RUN mkdir -p /app
WORKDIR /app
COPY package*.json /app
RUN npm ci
COPY . .

RUN npm run build
RUN npm prune --production

FROM node:20-alpine
RUN adduser -D nodeuser
RUN mkdir -p /app
RUN chown nodeuser:nodeuser /app
WORKDIR /app
COPY --from=build --chown=nodeuser:nodeuser /app/build build/
COPY --from=build --chown=nodeuser:nodeuser /app/node_modules node_modules/
COPY package.json .

EXPOSE 3000
CMD ["node", "build"]

The process uses multi-stage builds to reduce image size. Let’s look at the first build stage:

FROM node:20-alpine as build
RUN mkdir -p /app
WORKDIR /app
COPY package*.json /app
RUN npm ci
COPY . .

RUN npm run build
RUN npm prune --production

The first stage is named build. The first line means it uses Alpine OS to install Node version 20. The reason to use Alpine OS is because the size is much smaller compared to other Linux based operating systems. In the next step, it creates a new directory /app, change the working directory to it, then copy both package.json and package-lock.json files into the /app directory. Later, it runs npm ci (Clean Install) command to install dependencies from package-lock.json and copy the result into /app directory. Finally, it executes npm run build to build the project and npm prune --production to remove unnecessary dependencies.

FROM node:20-alpine
RUN adduser -D nodeuser
RUN mkdir -p /app
RUN chown nodeuser:nodeuser /app
WORKDIR /app
COPY --from=build --chown=nodeuser:nodeuser /app/build build/
COPY --from=build --chown=nodeuser:nodeuser /app/node_modules node_modules/
COPY package.json .

EXPOSE 3000
CMD ["node", "build"]

In the next stage, the script will create a new user nodeuser to own the /app directory and its subdirectories and to run the application. The script copies /app/build and /app/node_modules directories from the build stage (previous stage) into the specified directories. The EXPOSE command is to inform the reader that the application runs on port 3000. The script ends by running a command node build to start the application.

Using Docker Compose

I use docker compose to start the service easier. Docker compose is a tool to start a docker image by reading docker-compose.yaml file.

version: "3"
services:
  frontend:
    container_name: tabunganku-frontend
    build:
      context: .
      dockerfile: Dockerfile
    image: tabunganku-frontend:latest
    ports:
      - 3000:3000
    env_file:
      - .env
    networks:
      fe-network:
        ipv4_address: 172.20.10.4
networks:
  fe-network:
    name: tabunganku-network
    external: true

This is the content of docker-compose.yaml in Tabunganku frontend. It defines one service and one network. In the network side, it creates a network stack named fe-network, which refers to tabunganku-network. tabunganku-network is defined in the backend stack, so I add the external: true parameter in the network definition.

There is only 1 service defined in the service stack: frontend. I can define how to build the Docker image here. In this script, I build the Docker image by using the same directory as where docker-compose.yaml file is currently located and reading Dockerfile to create the image. The Docker image and container will be named tabunganku-frontend. It reads .env file for the environment variables, and will run on port 3000 in the fe-network with a static IP address 172.20.10.4.

To run the container, go to the project’s main directory and run command docker compose up.

  • Docker multi-stage builds: link
  • Docker compose: link
  • Dockerfile: link
  • npm commands: link
  • adapter-node: link
  • Alpine Linux: link