dockerfile best practices #

never build image with root user at runtime #

please keep in mind that default Dockerfile will ALWAYS run as root user

what happens if you run a container with root user, and deploy it to production?

A process running as root user inside a container is actually a process running as root user on the host machine. This means that if an attacker manages to exploit a vulnerability in your dockerize application, they will have root access to the host machine.

instead, use a non-root user but with the sudo privilege to prevent this:

Remove the sudo support part if you don't need it in runtime

ARG USERNAME=user-name-goes-here
ARG USER_UID=1000
ARG USER_GID=$USER_UID

RUN groupadd --gid $USER_GID $USERNAME \
    && useradd --uid $USER_UID --gid $USER_GID -m $USERNAME \
    # [Optional] Add sudo support for the non-root user
    && apt-get update \
    && apt-get install -y sudo \
    && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME \
    && chmod 0440 /etc/sudoers.d/$USERNAME

# Set the default user. Omit if you want to keep the default as root.
USER $USERNAME

this is mainly refer from code.visualstudio.com/remote/advancedcontainers/add-nonroot-user

you can also read this post (vietnamese), really well written and clear explanation:

Tại sao nên chạy ứng dụng container với non-root user?

use multi-stage builds #

multi-stage builds are useful to reduce the size of the final image by using a smaller image to build the application and then copy the build artifacts to a smaller image

for example in a node.js application, you just want to deliver the build artifacts (the dist folder) and not the big boy node_modules folder

# build stage
FROM node:22 as build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# deploy stage (smaller image)
FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html

You can reduce the size from multiple GBs to just a few MBs :D

use .dockerignore file #

Docker will copy all files in the build context to the image, for example when you run COPY . . in the Dockerfile, Docker will copy all files in the build context to the image, including the md files or node_modules folder,... which is not needed in the image

to prevent this, you can use a .dockerignore file to exclude files and folders from the build context

*.md
node_modules
docker-compose.yml
# ...

RUN multiple commands in a single RUN #

More layers in the image means more space, so it's better to combine multiple commands in a single RUN instruction

  • Method 1: using && and \ as a line continuation character
RUN apt-get update && apt-get install -y \
    package1 \
    package2 \
    package3
  • Method 2: using <<EOF...EOF as escape characters
RUN <<EOF
apt-get update
apt-get install -y \
    package1 \
    package2 \
    package3
EOF

use ARG instead of ENV #

Best practice is to use ARG instead of ENV to pass build-time variables to the Dockerfile

For example the node version use case:

ARG NODE_VERSION=22
FROM node:${NODE_VERSION}

And with this, you can build the image with a different node version:

docker build --build-arg NODE_VERSION=20 -t my-node-app .