Docker & Container Concept

Containers

Containers consists of:

  1. chroot (read:change root): to limit file system access of a root in a directory

  2. namespace: chroot doesn't stop other roots to see all the processes going on the computer. Namespace allow us to hide processes from other processes. If we give each chroot-ed environemnt different sets of namespace, each environment won't be able to see processes from toher environment. It is done with the command unshare

    ```js
    unshare --mount --uts --ipc --net --pid --fork --user --map-root-user chroot /better-root bash
    ```
    which is followed by the chroot command.
  3. cgroups/control groups: protect runaway processes from taking down entire server. It is because without it, there is no isolation of physical components from these environments. So we can limit a bunch of stuff, limit CPU, memory, network, etc.

Docker

Docker run vs exec

Docker run will start a new container, while docker exec will run a command in an existing container.

Tags

Tag marks the version we're using, usually we don't use the latest. A good guideline is to use the LTS version for the OS and the programming language you're using. For example, if we'd like eto use Node, we can choose the 12-stretch version as node 12 is the LTS of node and stretch is the LTS version of Debian.

Dockerfile

Basic

It is a list of instructions starting with FROM keyword which denotes the base image.

FROM node:12-stretch
CMD ["node", "-e", "console.log(\"omg hi lol\")"]

-e means immediately run the code after that. Only the last CMD gets executed.

After that to run the Dockerfile, we run docker build . in the directory where the dockerfile is located in. Then, we can run the container docker run <id> and see the console log result. We can override the CMD command by docker run <id> <command> such as docker run <id> ls.

Instead of refering the ID, we can assign the name to a container when we build it using the --tag options, like docker build --tag my-app .. We can also add the tag of the container there to add versioning, like docker build --tag my-app:1 ..

Build node.js app

Create an index.js file which contains a simple server.

FROM node:12-stretch
COPY index.js index.js
CMD ["node", "index.js"]

COPY is to copy from source to destination. Again build and run the container. --init is to run the module TINI module to handle shutdown signal sent to node (proxy the process and automatically shut down the node process for you). It is because node doesn't respond to Ctrl+C by default. We also need to expose the port so that we can access the server on our browser by using --publish.

docker run --init --rm --publish 3000:3000 <id>
Security concerns

We also need to remember that we should never run as root, thus the node container maker provides a user named node to run the container as node user and own the files copied.

FROM node:12-stretch
USER node
COPY --chown=node:node index.js index.js
CMD ["node", "index.js"]
COPY VS ADD
COPY --chown=node:node index.js index.js
ADD --chown=node:node index.js index.js

These two commands are doing similar things, however ADD is more powerful as it can go out to the network, thus we can get files from github using ADD. It also automatically zip and unzip the files when we get it. Thus, if we need the network/unzip files, use ADD.

WORKDIR
FROM node:12-stretch
USER node
WORKDIR /home/node/code
COPY --chown=node:node index.js index.js
CMD ["node", "index.js"]

/home/node is the home directory of the user called node. After this, the index.js won't be copied into the root directory.

RUN

RUN is used whenever we want to run arbitrary shell command, such as installing dependency.

FROM node:12-stretch
USER node
RUN mkdir /home/node/code
WORKDIR /home/node/code
COPY --chown=node:node . .
RUN npm ci
CMD ["node", "index.js"]

The RUN mkdir /home/node/code is meant to create a folder called code with the current user as the owner instead of root. If this folder is owned by the root, we would not be able to install dependencies (since it creates node_modules directory).

EXPOSE

Expose is to replace the publish 3000:3000 argument when we're running the docker. We also need to add the expose argument -P when running the docker image. Then we can see which port is chosen by the docker to expose by using docker ps.

EXPOSE 3000
Layers

Docker container is composed of layers. When we re-run a build process, it is smart enough to see which instructions haven't changed and won't change if you run it again so it uses the same containers it cached.

However, in the previous example, let's say we change one line of code, then it will see that the COPY is different due to the one line chnage and it begins the build process there and re-runs all instructions after that.

This will also mean that we would re-run the npm ci even though the dependencies haven't changed. Thus, we can choose to break the copy into two parts.

COPY --chown=node:node package-lock.json package.json ./
RUN npm ci
COPY --chown=node:node . .

The first COPY pulls just the package.json and the package-lock.json which is just enough to do the npm ci. After that we add the rest of the files. Now if you make changes you can avoid doing a full npm install.

Docker ignore

It is like .gitignore, to ignore files we don't want to copy over. It is kept in the .dockerignore file.

Multistage Build

It's like building something, copy the output to another container. It is because, sometimes we don't need so many commands once the files are built. One of the example is the case below, we won't be needing npm to run the file built. We only need node. Therefore, instead of using the base image node:12-scretch which contains OS + node + npm + several other tools, what we can do is to build all the files using that base image, then copy the build files into another container which has smaller sized base image, like the alpine. After that, we just install node, which is the only thing we need to run the build files. The --from tag is meant to copy files from the other stage.

# build stage
FROM node:12-stretch
WORKDIR /build
COPY package-lock.json package.json ./
RUN npm ci
COPY . .
# runtime stage
FROM alpine:3.10
RUN apk add --update nodejs
RUN addgroup -S node && adduser -S node -G node
USER node
RUN mkdir /home/node/code
WORKDIR /home/node/code
COPY --from=0 --chown=node:node /build .
CMD ["node", "index.js"]