Docker & Container Concept
Containers
Containers consists of:
chroot (read:change root): to limit file system access of a root in a directory
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
```jsunshare --mount --uts --ipc --net --pid --fork --user --map-root-user chroot /better-root bash```which is followed by the chroot command.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-stretchCMD ["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-stretchCOPY index.js index.jsCMD ["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-stretchUSER nodeCOPY --chown=node:node index.js index.jsCMD ["node", "index.js"]
COPY VS ADD
COPY --chown=node:node index.js index.jsADD --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-stretchUSER nodeWORKDIR /home/node/codeCOPY --chown=node:node index.js index.jsCMD ["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-stretchUSER nodeRUN mkdir /home/node/codeWORKDIR /home/node/codeCOPY --chown=node:node . .RUN npm ciCMD ["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 ciCOPY --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 stageFROM node:12-stretchWORKDIR /buildCOPY package-lock.json package.json ./RUN npm ciCOPY . .# runtime stageFROM alpine:3.10RUN apk add --update nodejsRUN addgroup -S node && adduser -S node -G nodeUSER nodeRUN mkdir /home/node/codeWORKDIR /home/node/codeCOPY --from=0 --chown=node:node /build .CMD ["node", "index.js"]