Dockerfile instructions

Reference: Dockerfile reference, Best practices for Dockerfile instructions


  • Use the user:group form for the USER instruction, unless you want the group to be root.

  • Use the name runner for the non-root user:

    RUN groupadd -r runner && useradd --no-log-init -r -g runner runner


  • Use a leading / with the WORKDIR instruction.

  • Set WORKDIR to /workdir:

    WORKDIR /workdir


  • Use the --chown=user:group option with the COPY instruction, unless you want the ownership of the files to be root:root.

  • Prefer the COPY instruction to the ADD instruction, as recommended.

Layer order

To leverage the build cache, order the instructions from least-to-most likely to change over time. In general, the order is:

  1. Declare the base image

  2. Install system packages

  3. Create a non-root user

  4. Copy requirements files

  5. Install project dependencies

  6. Set the working directory

  7. Switch to the non-root user

  8. Copy project files


For Node, set the working directory before copying requirements files.

Base images

For Python, use the default image, as recommended, of the minor version, to ensure predictable behavior. Do not use the -slim or -alpine versions.

For Node, use the default image, as recommended, of the major version. Do not use the -slim or -alpine versions.

For a web server, use the nginxinc/nginx-unprivileged:latest image. Note that the default port is changed to 8080 (instead of 80).

Set server_tokens off; to prevent false positives from penetration tests (Ubuntu backports security patches, without changing version numbers).


For reference, the default /etc/nginx/conf.d/default.conf file in the Nginx image is:

server {
    listen       8080;
    listen  [::]:8080;
    server_name  localhost;

    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;

    #error_page  404              /404.html;

    # redirect server error pages to the static page /50x.html
    error_page   500 502 503 504  /50x.html;
    location = /50x.html {
        root   /usr/share/nginx/html;

    # proxy the PHP scripts to Apache listening on
    #location ~ \.php$ {
    #    proxy_pass;

    # pass the PHP scripts to FastCGI server listening on
    #location ~ \.php$ {
    #    root           html;
    #    fastcgi_pass;
    #    fastcgi_index  index.php;
    #    fastcgi_param  SCRIPT_FILENAME  /scripts$fastcgi_script_name;
    #    include        fastcgi_params;

    # deny access to .htaccess files, if Apache's document root
    # concurs with nginx's one
    #location ~ /\.ht {
    #    deny  all;

See also

nginx playground

System packages

Before installing a system package, check whether it’s included in a base image. For example, the psycopg2 Python package requires the libpq-dev system package. To check whether it’s included, when using the python:3.10 image:

  1. Find the tag on the DockerHub page of the base image (the 3.10 tag is under Shared Tags)

  2. Click the link to view the Dockerfile

  3. Check the apt-get install commands for the package name

  4. If not found, look for FROM instructions

  5. Repeat from step 1 for the FROM image(s)

We find that the buildpack-deps:bullseye image installs the libpq-dev system package.

If it’s not included, install it following best practices:

RUN apt-get update && apt-get install -y --no-install-recommends \
      package-a \
      package-b \
      package-c \
   && rm -rf /var/lib/apt/lists/*

Bind mounts


In general, do not use absolute paths on the host’s filesystem as an API between projects, because the projects might not share the same filesystem, like in the case of Docker containers. Instead, use paths relative to a configurable setting.

If a project needs to read or write data to the filesystem:

  1. Add a setting with a default value. For example, for a Django project:
        "KINGFISHER_COLLECT_FILES_STORE", "/data" if production else BASE_DIR / "data"
  2. Create the directory using the default value in the Dockerfile. For example:

    # Must match the settings.KINGFISHER_COLLECT_FILES_STORE default value.
    RUN mkdir -p /data && chown -R runner:runner /data
  3. Mount the host’s directory to the default value in the Docker Compose file. For example:

          - /data/storage/kingfisher-collect:/data

If a project needs to read or write data to multiple directories, set the default values to subdirectories of the /data directory.



If Dockerfiles are similar across projects, we can consider creating our own base images and using the ONBUILD instruction to copy source code.


FROM python:3.11

RUN groupadd -r runner && useradd --no-log-init -r -g runner runner

COPY requirements.txt /tmp/requirements.txt
RUN pip install --no-cache-dir -r /tmp/requirements.txt

WORKDIR /workdir
USER runner:runner
COPY --chown=runner:runner . .