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 bemy/
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 usingcd
:- 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
andADD
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
orrm -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 useRUN 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 .
- override it:
- ENV can be reachable during the run of the container
- override it:
docker run --env ENV_KEY=ENV_VALUE
- override it:
CMD
vs ENTRYPOINT
CMD
: Dockerfile applies the instructions from the last one: Can be easily overrideENTRYPOINT
: 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.
- Consequently it is better to use
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>
\ ordocker build --build-arg REGISTRY_USER=$USER --build-arg REGISTRY_PASSWORD=$PASSWORD
- step 1.
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:
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}