Docker Best Practices

Docker Best Practices

·

5 min read

Best Practises and Tips

Best practice when writing a Dockerfile

Create ephemeral containers:

  • Process oriented (no persistent storage)
  • Can be stopped and destroyed, then rebuilt and replace with an absolute minimum set up and configuration

Create a dedicated directory for Dockerfile with only necessary file.

Cause all files and directories are sent to the docker daemon which increase the processing time.

  • Dockerfile with other unnecessary files aside:

    $ docker build -t test:test .
    Sending build context to Docker daemon    512MB
    Step 1/3 : FROM alpine:latest
    ...
    
  • Dockerfile in is own directory

    $ docker build -t test:test2 . 
    Sending build context to Docker daemon  4.608kB
    Step 1/3 : FROM alpine:version
    ...
    
  • The context is built regardless of where the Dockerfile is. That means if you use: docker build -f my/path/to/Dockerfile . the context will be my/

Pipe Dockerfile trough stdin

  • You can use:

    echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
    

    or

    docker build -<<EOF
    FROM busybox
    RUN echo "hello world"
    EOF
    

If you don't need a persistent image or if you don't want to write the Dockerfile on the disk

  • You can also use pipe and hyphen (-) to specify the context, and not have to move on the specific directory using cd:

    • Classic way
    $ docker build -f /home/user/workspace/tests/Dockerfile .
    Sending build context to Docker daemon    512MB
    Step 1/3 : FROM alpine:version
    ...
    
    • Using pipe:
    $ cat /home/user/workspace/tests/Dockerfile | docker build -t test:test2 -
    Sending build context to Docker daemon  2.048kB
    Step 1/3 : FROM alpine:version
    ...
    
  • To optimize your docker context you can also use the .dockerignore file

Build your docker image from a git context:

  • Example:

    docker build -t myimage:latest -f- https://github.com/docker-library/hello-world.git <<EOF
    FROM busybox
    COPY hello.c ./
    EOF
    

    or

    $ cat Dockerfile | docker build -t test:test -f- https://github.com/docker-library/hello-world.git
    

    This feature requires git to be installed

Use multi-stage builds. This feature can be useful if you want to build and run your image in a single Dockerfile.

Example with a node Dockerfile:

FROM node:12
ADD . /app
WORKDIR /app
RUN npm install
EXPOSE 8080
CMD [ "node", "server.js" ]

Separate in two steps: build then run using distroless images

#First stage

FROM node:12 AS stage1
ADD . /app
WORKDIR /app
RUN npm install

#Second Stage

FROM gcr.io/distroless/nodejs
COPY --from=stage1 /app /app
WORKDIR /app
EXPOSE 8080
CMD ["server.js"]

Here is an example using java and distroless (source here)

FROM openjdk:11-jdk-slim AS build-env
COPY . /app/examples
WORKDIR /app
RUN javac examples/*.java
RUN jar cfe main.jar examples.HelloJava examples/*.class 

FROM gcr.io/distroless/java:11
COPY --from=build-env /app /app
WORKDIR /app
CMD ["main.jar"]

General tips

  • Don't install unnecessary packages
  • Decouple application
  • Limit each container to one process
  • Web example:
    • web application
    • database
    • in-memory cache
  • Minimize the number of layers
  • Only RUN, COPY and ADD create layers.
  • Sort multi-line arguments:
RUN apt-get update && apt-get install -y \
      bzr \
      cvs \
      git \
      mercurial \
      subversion \
    && rm -rf /var/lib/apt/lists/*

Package manager tips

apt

  • combine update an installation in the same line: RUN apt-get update && apt-get install -y vim
  • end with rm -rf /var/lib.apt/lists/*

apk:

  • Install using: apk add --no-cache vim

yum:

  • end with yum clean -y all or rm -rf /var/cache/* or both
  • Using pipes: Docker execute this command : RUN wget -O - https://some.site | wc -l > /number using /bin/sh -c. Which only evaluates the exit code of the last operation. To take all operation into account use RUN set -o pipefail && wget -O - https://some.site | wc -l > /number

TIPS

ARG vs ENV

  • They both can be used during the build
  • ARG is only reachable during the build an not during the run of the container
    • override it: docker build --build-arg ARG_KEY=ARG_VALUE -t image:tag .
  • ENV can be reachable during the run of the container
    • override it: docker run --env ENV_KEY=ENV_VALUE

CMD vs ENTRYPOINT

  • CMD: Dockerfile applies the instructions from the last one: Can be easily override
  • ENTRYPOINT: Cannot be override

ADD vs COPY

  • COPY only support basic copying of local file into the container
    • Consequently it is better to use COPY for basic copy
    • When using COPY it is better to copy each file individually. This ensures that each step’s build cache is only invalidated (forcing the step to be re-run) if the specifically required files change.
  • ADD has some additional feature
    • local-only tar extraction
    • remote URL
  • Example: For .jar file stores in a standard registry like artifactory for instance:\ With curl

    • step 1. curl -k ${REGISTRY_USER}:${REGISTRY_PASSWORD} -v -XGET ${REGISTRY_URL/path/to/my-app.jar >> ./app/my-app.jar
    • step 2. docker build
    FROM openjdk:11
    WORKDIR /app
    COPY ./app/my-app.jar my-app.jar
    CMD ["java","-jar","/app/my-app.jar"]
    

    \ Using ADD:

    FROM openjdk:11
    ARG REGISTRY_USER 
    ARG REGISTRY_PASSWORD
    WORKDIR /app
    ADD  http://${REGISTRY_USER}:${REGISTRY_PASSWORD}@my-registry.com/path/to/my-app.jar my-app.jar
    ENTRYPOINT ["java","-jar","/app/my-app.jar"]
    

    When docker build do not forget to specify argument:

    • docker build --build-arg REGISTRY_USER=<user> --build-arg REGISTRY_PASSWORD=<password> \ or
    • docker build --build-arg REGISTRY_USER=$USER --build-arg REGISTRY_PASSWORD=$PASSWORD

ENV instruction

  • can be used to update path: ENV PATH=/usr/local/nginx/bin:$PATH
  • can map a host variable: ENV http_proxy=${http_proxy}

Build an image inside a gitlab job using kaniko:

Official documentation

build:
    stage: docker-build
    image:
        name: gcr.io/kaniko-project/executor:debug
        entrypoint: [""]
    variables:
      REGISTRY_URL: https://my-registry.com
      REGISTRY_REPO: my-repo
      REGISTRY_DST: my-registry.com
      # REGISTRY_USER ## Should be declared in the gitlab-ci vars section
      # REGISTRY_PASSWORD ## Should be declared in the gitlab-ci vars section
    script:
        - mkdir -p /kaniko/.docker
        - echo "{\"auths\":{\"${REGISTRY_URL}\":{\"username\":\"${REGISTRY_USER}\",\"password\":\"${REGISTRY_PASSWORD}\"}}}"
        - echo "{\"auths\":{\"${REGISTRY_DST}\":{\"username\":\"${REGISTRY_USER}\",\"password\":\"${REGISTRY_PASSWORD}\"}}}" > /kaniko/.docker/config.json
        - /kaniko/executor
            --build-arg REGISTRY_USER=$REGISTRY_USER
            --build-arg REGISTRY_PASSWORD=$REGISTRY_PASSWORD
            --context $CI_PROJECT_DIR 
            --dockerfile ${CI_PROJECT_DIR}/Dockerfile 
            --destination ${REGISTRY_DST}/${REGISTRY_REPO}/${CI_PROJECT_TITLE}:${CI_PIPELINE_ID}