Docker
Useful commands and tips for working with Docker.
Common Docker commands
| Command | Purpose |
|---|---|
docker run <image> |
Create and start a container from an image. |
docker ps [-a] [-s] |
Lists running containers. |
docker stop <container> |
Gracefully stops a running container. |
docker start <container> |
Restarts a stopped container. |
docker rm <container> |
Removes a container. |
docker exec -it <container> bash |
Provides shell access to a running container. |
docker logs <container> |
Displays a container’s logs. |
docker stats |
Shows a live stream of container(s) resource usage statistics. |
To see a full list of Docker commands and their options, see the Dockerfile CLI reference.
Common Dockerfile instructions
| Instruction | Purpose |
|---|---|
FROM image_name |
Specifies the base image to use for the new image. |
WORKDIR /some/path |
Sets the working directory for the instructions that follow. |
COPY <src> <dest> |
Copies files or directories from the build context to the image. |
RUN <command> |
Executes commands in the shell during image builds. |
EXPOSE <port> |
Port(s) Docker will be listening on at runtime. |
ENV KEY=VALUE |
Sets environment variables. |
USER user |
Set user and group ID. |
CMD <command> |
The default command to execute when the container starts. |
ENTRYPOINT <command> |
Similar as CMD, but cannot be overriden. |
To see a full list of Dockerfile instructions, see the Dockerfile reference.
Dockerfile tips
The following tips suggest various best practices for writing Dockerfiles.
Using smaller base images
Many popular Docker images these days have an Alpine variant. This means that
the image is based on the official alpine image on Docker hub,
based on the Alpine Linux distribution. Alpine Linux is much smaller
than most distribution base images (~5MB), and thus leads to much slimmer images
in general.
FROM node:24-alpine
Here we use the node:24-alpine tag instead of simply node:24.
These variants are highly recommended when final image size being as small as possible is desired.
The main caveat to note is that Alpine Linux uses musl libc instead of glibc and friends, so certain software might run into compilation issues depending on the depth of their libc requirements. However, most software doesn’t have an issue with this, so this variant is usually a very safe choice. See this Hacker News comment thread for more discussion of the issues that might arise and some pro/con comparisons of using Alpine-based images.
To minimize image size, it’s uncommon for additional related tools (such as Git or Bash) to be included in Alpine-based images. Using this image as a base, add the things you need in your own Dockerfile (see the alpine image description for examples of how to install packages if you are unfamiliar).
Labeling images
Labels are metadata attached to images and containers. They can be used to
influence the behavior of some commands, such as docker ps. You can add labels
from a Dockerfile with the LABEL instruction.
A popular convention is to add a org.opencontainers.image.authors label to
provide an author (and potential maintenance contact e-mail):
LABEL org.opencontainers.image.authors="john.doe@example.com"
You may see the labels of an image or container with docker inspect.
You may also filter containers by label. For example, to see all running
containers that have the foo label set to the value bar, you can use the
following command:
docker ps -f label=foo=bar
Environment variables
The ENV instruction allows you to set environment variables. Many applications
change their behavior in response to some variables. For example, a Node.js
application might run in production mode if the $NODE_ENV variable is set to
production. Additionally, it might listen on the port specified by the $PORT
variable.
Here’s how you could set both variables:
ENV NODE_ENV=production \
PORT=3000
Non-root users
All commands run by a Dockerfile (RUN and CMD instructions) are run by the
root user of the container by default. This is not a good idea as any
security flaw in your application may give root access to the entire container
to an attacker.
The security impact of this would be mitigated since the container is isolated from the host machine, but it could still be a severe security issue depending on your container’s configuration.
Therefore, it is good practice to create an unprivileged user to run your
application even in the container. Here we use Alpine Linux’s addgroup and
adduser commands to create a user, and make sure that your application’s
directory, e.g. /app, where you copy the application is owned by that user:
RUN addgroup -S app && \
adduser -S -G app app && \
mkdir -p /app && \
chown app:app /app
(Note that these commands are specific to Alpine Linux. You would use groupadd
and useradd on Ubuntu, for example, which use different options.)
Finally, we use the USER instruction to make sure that all further commands
run in this Dockerfile (by RUN or CMD instructions) are executed as the new
user instead of the root user:
USER app:app
When using the COPY command, you can use the --chown=app:app flag to copy
files and set their ownership in one go.
Speeding up builds
The following pattern is popular to speed up builds of applications that use a package manager (e.g. npm, RubyGems, Composer).
Installing packages is often one of the slowest command to run for small applications, so we want to take advantage of Docker’s build cache as much as possible to avoid running it every time. Suppose you did this like in the Dockerfile for a Node.js application:
COPY ./ /app/
WORKDIR /app
RUN npm ci
Every time you make the slightest change in any of the application’s files, the
COPY instruction’s cache, and all further commands’ caches will be
invalidated, including the cache for the RUN npm ci instruction. Therefore,
any change will trigger a full installation of all dependencies from scratch.
To improve this behavior, you can split the installation of your application in
the container into two parts. The first part is to copy only the package
manager’s files (in this case package.json and package-lock.json) into the
application’s directory, and to run an npm ci command like before:
COPY package.json package-lock.json /app/
WORKDIR /app
RUN npm ci
Now, if a change is made to the package.json or package-lock.json files, the
cache of the RUN npm ci instruction will be invalidated like before, and the
dependencies will be re-installed, which we want since the change was probably a
dependency update. However, changes in any other file of the application will
not invalidate the cache for those 3 instructions, so the result of the RUN npm
ci instruction will remain cached.
The second part of the installation process is to copy the rest of your application into the directory:
COPY ./ /app/
Now, if any file in your application changes, the cache of further instructions
will be invalidated, but since the RUN npm ci instruction comes before, it
will remain in the cache and be skipped at build time (unless you modify the
package.json or package-lock.json files).
Documenting exposed ports
The EXPOSE instruction informs Docker that the container listens on the
specified network ports at runtime.
EXPOSE 3000
The EXPOSE instruction does not actually publish the port. It functions as a
type of documentation between the person who builds the image and the person who
runs the container, about which ports are intended to be published. To actually
publish the port when running the container, use the -p option on docker run
to publish and map one or more ports, or the -P option to publish all exposed
ports and map them to high-order ports.