Dockerfile if else条件与外部参数

我有dockerfile

FROM centos:7
ENV foo=42

然后我建立它

docker build -t my_docker .

然后运行它。

docker run -it -d  my_docker

是否可以从命令行传递参数,并在Dockerfile中使用if else ?我的意思是

FROM centos:7
if (my_arg==42)
{ENV=TRUE}
else:
{ENV=FALSE}

用这个论证来构建。

 docker build -t my_docker . --my_arg=42
344801 次浏览

根据docker build命令的文档,有一个名为--build-arg的参数。

使用示例:

docker build --build-arg HTTP_PROXY=http://10.20.30.2:1234 .

在我看来,这正是你所需要的:)

它可能看起来不那么干净,但你可以让你的Dockerfile(有条件的)如下所示:

FROM centos:7
ARG arg
RUN if [[ -z "$arg" ]] ; then echo Argument not provided ; else echo Argument is $arg ; fi

然后将图像构建为:

docker build -t my_docker . --build-arg arg=45

docker build -t my_docker .

直接使用“test”二进制代码就可以做到这一点。如果你不想指定一个“else”条件,你也应该使用noop命令“:”,这样docker就不会因为一个非零的返回值错误而停止。

RUN test -z "$YOURVAR" || echo "var is set" && echo "var is not set"
RUN test -z "$YOURVAR" && echo "var is not set" || :
RUN test -z "$YOURVAR" || echo "var is set" && :

出于某种原因,这里的大多数答案都没有帮助我(可能与Dockerfile中的From图像有关)

所以我更喜欢在我的工作空间中创建bash script--build-arg,以便在Docker构建时通过检查参数是否为空来处理if语句

Bash脚本:

#!/bin/bash -x


if test -z $1 ; then
echo "The arg is empty"
....do something....
else
echo "The arg is not empty: $1"
....do something else....
fi

Dockerfile:

FROM ...
....
ARG arg
COPY bash.sh /tmp/
RUN chmod u+x /tmp/bash.sh && /tmp/bash.sh $arg
....

码头工人构建:

docker build --pull -f "Dockerfile" -t $SERVICE_NAME --build-arg arg="yes" .

注意:这将被用于bash脚本中的else (false)

docker build --pull -f "Dockerfile" -t $SERVICE_NAME .

备注:这将进入if (true)

编辑1:

经过几次尝试,我找到了以下文章和这个一个 这帮助我理解了两件事:

1) FROM之前的ARG在构建之外

2)默认shell是/bin/sh,这意味着if else在docker构建中有一点不同。例如,你只需要一个“=”而不是“==”来比较字符串。

所以你可以在Dockerfile中做这个

ARG argname=false   #default argument when not provided in the --build-arg
RUN if [ "$argname" = "false" ] ; then echo 'false'; else echo 'true'; fi

docker build中:

docker build --pull -f "Dockerfile" --label "service_name=${SERVICE_NAME}" -t $SERVICE_NAME --build-arg argname=true .

正如其他人所说,shell脚本会有所帮助。

只是一个额外的情况,恕我直言,这是值得一提的(对于那些偶然发现这里的人,寻找一个更简单的情况),那是< >强环境替换< / >强

环境变量(用ENV语句声明)也可以在某些指令中用作由Dockerfile解释的变量。

${variable_name}语法还支持如下所述的一些标准bash修饰符:

  • ${variable:-word}表示如果设置了variable,则结果将是该值。如果variable未设置,则结果为word

  • ${variable:+word}表示如果设置了variable,则结果为word,否则结果为空字符串。

使用Bash脚本和Alpine/Centos

Dockerfile

FROM alpine  #just change this to centos


ARG MYARG=""
ENV E_MYARG=$MYARG


ADD . /tmp
RUN chmod +x /tmp/script.sh && /tmp/script.sh

script.sh

#!/usr/bin/env sh


if [ -z "$E_MYARG" ]; then
echo "NO PARAM PASSED"
else
echo $E_MYARG
fi

< >强传递参数: docker build -t test --build-arg MYARG="this is a test" . < / p >

....
Step 5/5 : RUN chmod +x /tmp/script.sh && /tmp/script.sh
---> Running in 10b0e07e33fc
this is a test
Removing intermediate container 10b0e07e33fc
---> f6f085ffb284
Successfully built f6f085ffb284

< >强没有参数: docker build -t test . < / p >

....
Step 5/5 : RUN chmod +x /tmp/script.sh && /tmp/script.sh
---> Running in b89210b0cac0
NO PARAM PASSED
Removing intermediate container b89210b0cac0
....

接受的答案可以解决这个问题,但如果你想在dockerfile中有多行if条件,你可以把\放在每行的末尾(类似于你在shell脚本中的做法),并以;结束每个命令。你甚至可以定义像set -eux这样的命令作为第一个命令。

例子:

RUN set -eux; \
if [ -f /path/to/file ]; then \
mv /path/to/file /dest; \
fi; \
if [ -d /path/to/dir ]; then \
mv /path/to/dir /dest; \
fi

在你的情况下:

FROM centos:7
ARG arg
RUN if [ -z "$arg" ] ; then \
echo Argument not provided; \
else \
echo Argument is $arg; \
fi

然后使用:

docker build -t my_docker . --build-arg arg=42

对于所提出的解决方案,有一个有趣的替代方案,它与单一Dockerfile一起工作,每个条件构建只需要对docker build的单个调用避免bash

解决方案:

下面的Dockerfile解决了这个问题。复制粘贴,自己试试。

ARG my_arg


FROM centos:7 AS base
RUN echo "do stuff with the centos image"


FROM base AS branch-version-1
RUN echo "this is the stage that sets VAR=TRUE"
ENV VAR=TRUE


FROM base AS branch-version-2
RUN echo "this is the stage that sets VAR=FALSE"
ENV VAR=FALSE


FROM branch-version-${my_arg} AS final
RUN echo "VAR is equal to ${VAR}"

Dockerfile说明:

我们首先得到一个base图像(在你的例子中是centos:7),并把它放到自己的阶段中。base阶段应该包含在条件之前要做的事情。在这之后,我们还有两个阶段,代表条件的分支:branch-version-1branch-version-2。我们两个都做了。final阶段基于my_arg选择其中一个阶段。条件Dockerfile。好了。

运行时输出:

(我把它缩写了一下…)

my_arg==2

docker build --build-arg my_arg=2 .
Step 1/12 : ARG my_arg
Step 2/12 : ARG ENV
Step 3/12 : FROM centos:7 AS base
Step 4/12 : RUN echo "do stuff with the centos image"
do stuff with the centos image
Step 5/12 : FROM base AS branch-version-1
Step 6/12 : RUN echo "this is the stage that sets VAR=TRUE"
this is the stage that sets VAR=TRUE
Step 7/12 : ENV VAR=TRUE
Step 8/12 : FROM base AS branch-version-2
Step 9/12 : RUN echo "this is the stage that sets VAR=FALSE"
this is the stage that sets VAR=FALSE
Step 10/12 : ENV VAR=FALSE
Step 11/12 : FROM branch-version-${my_arg}
Step 12/12 : RUN echo "VAR is equal to ${VAR}"
VAR is equal to FALSE

my_arg==1

docker build --build-arg my_arg=1 .
...
Step 11/12 : FROM branch-version-${my_arg}
Step 12/12 : RUN echo "VAR is equal to ${VAR}"
VAR is equal to TRUE

感谢Tõnis为这个惊人的想法!< / >

在可能的情况下,不要使用其他答案中描述的构建参数。这是一个古老的混乱的解决方案。Docker的target属性解决了这个问题。

目标的例子

Dockerfile

FROM foo as base


RUN ...


# Build dev image
FROM base as image-dev


RUN ...
COPY ...


# Build prod image
FROM base as image-prod


RUN ...
COPY ...
docker build --target image-dev -t foo .
version: '3.4'


services:


dev:
build:
context: .
dockerfile: Dockerfile
target: image-dev

现实世界中

dockerfile在现实世界中变得很复杂。使用buildkit &COPY --from用于更快、更可维护的dockerfile:

  • Docker在目标之上构建每个阶段,而不管它是否被继承。使用buildkit只构建继承的阶段。Docker必须是v19+。希望这很快会成为默认功能。
  • 目标可以共享构建阶段。使用COPY --from来简化继承。
FROM foo as base
RUN ...
WORKDIR /opt/my-proj


FROM base as npm-ci-dev
# invalidate cache
COPY --chown=www-data:www-data ./package.json /opt/my-proj/package.json
COPY --chown=www-data:www-data ./package-lock.json /opt/my-proj/package-lock.json
RUN npm ci


FROM base as npm-ci-prod
# invalidate cache
COPY --chown=www-data:www-data ./package.json /opt/my-proj/package.json
COPY --chown=www-data:www-data ./package-lock.json /opt/my-proj/package-lock.json
RUN npm ci --only=prod


FROM base as proj-files
COPY --chown=www-data:www-data ./ /opt/my-proj


FROM base as image-dev
# Will mount, not copy in dev environment
RUN ...


FROM base as image-ci
COPY --from=npm-ci-dev /opt/my-proj .
COPY --from=proj-files /opt/my-proj .
RUN ...


FROM base as image-stage
COPY --from=npm-ci-prod /opt/my-proj .
COPY --from=proj-files /opt/my-proj .
RUN ...


FROM base as image-prod
COPY --from=npm-ci-prod /opt/my-proj .
COPY --from=proj-files /opt/my-proj .
RUN ...

开启实验模式。

sudo echo '{"experimental": true}' | sudo tee /etc/docker/daemon.json

启用buildkit进行构建。Buildkit构建时默认不带缓存——使用--build-arg BUILDKIT_INLINE_CACHE=1启用

CI构建工作。

DOCKER_BUILDKIT=1 \
docker build \
--build-arg BUILDKIT_INLINE_CACHE=1 \
--target image-ci\
-t foo:ci
.

使用--cache-from从拉出的图像中缓存

生产工作

docker pull foo:ci
docker pull foo:stage


DOCKER_BUILDKIT=1 \
docker build \
--cache-from foo:ci,foo:stage \
--target image-prod \
-t prod
.

我在容器上设置代理服务器时遇到了类似的问题。

我使用的解决方案是一个入口点脚本和另一个环境变量配置脚本。使用RUN,您可以确保配置脚本在构建时运行,并在运行容器时运行ENTRYPOINT。

——build-arg用于命令行设置代理用户和密码。

因为我在容器启动时需要相同的环境变量,所以我使用了一个文件来“持久化”。它从构建到运行。

入口点脚本看起来像这样:

#!/bin/bash
# Load the script of environment variables
. /root/configproxy.sh
# Run the main container command
exec "$@"

configproxy.sh

#!/bin/bash


function start_config {
read u p < /root/proxy_credentials


export HTTP_PROXY=http://$u:$p@proxy.com:8080
export HTTPS_PROXY=https://$u:$p@proxy.com:8080


/bin/cat <<EOF > /etc/apt/apt.conf
Acquire::http::proxy "http://$u:$p@proxy.com:8080";
Acquire::https::proxy "https://$u:$p@proxy.com:8080";
EOF
}


if [ -s "/root/proxy_credentials" ]
then
start_config
fi

在Dockerfile中,配置:

# Base Image
FROM ubuntu:18.04


ARG user
ARG pass


USER root


# -z the length of STRING is zero
# [] are an alias for test command
# if $user is not empty, write credentials file
RUN if [ ! -z "$user" ]; then echo "${user} ${pass}">/root/proxy_credentials ; fi


#copy bash scripts
COPY configproxy.sh /root
COPY startup.sh .


RUN ["/bin/bash", "-c", ". /root/configproxy.sh"]


# Install dependencies and tools
#RUN apt-get update -y && \
#    apt-get install -yqq --no-install-recommends \
#    vim iputils-ping


ENTRYPOINT ["./startup.sh"]
CMD ["sh", "-c", "bash"]

在没有代理设置的情况下构建

docker build -t img01 -f Dockerfile .

使用代理设置构建

docker build -t img01 --build-arg user=<USER> --build-arg pass=<PASS> -f Dockerfile .

来看看在这里

你可以添加一个简单的检查:

RUN [ -z "$ARG" ] \
&& echo "ARG argument not provided." \
&& exit 1 || exit 0

我看到了很多可能的解决方案,但没有一个适合我今天面临的问题。所以,我正在花时间用另一个对我有用的可能的解决方案来回答这个问题。

在我的例子中,我利用了众所周知的if [ "$VAR" == "this" ]; then echo "do that"; fi。警告是Docker,我不知道为什么,在这种情况下不喜欢双等号。所以我们需要这样写if [ "$VAR" = "this" ]; then echo "do that"; fi

这里有一个完整的例子,适用于我的情况:

FROM node:16


# Let's set args and envs
ARG APP_ENV="dev"
ARG NPM_CMD="install"
ARG USER="nodeuser"
ARG PORT=8080
ENV NPM_CONFIG_PREFIX=/home/node/.npm-global
ENV PATH=$PATH:/home/node/.npm-global/bin
ENV NODE_ENV=${APP_ENV}


# Let's set the starting point
WORKDIR /app


# Let's build a cache
COPY package*.json .
RUN date \
# If the environment is production or staging, omit dev packages
# If any other environment, install dev packages
&& if [ "$APP_ENV" = "production" ]; then NPM_CMD="ci --omit=dev"; fi \
&& if [ "$APP_ENV" = "staging" ]; then NPM_CMD="ci --omit=dev"; fi \
&& npm ${NPM_CMD} \
&& usermod -d /app -l ${USER} node


# Let's add the App
COPY . .


# Let's expose the App port
EXPOSE ${PORT}


# Let's set the user
USER ${USER}


# Let's set the start App command
CMD [ "node", "server.js" ]

因此,如果用户传递正确的构建参数,docker build命令将为生产应用创建一个映像。如果没有,它将创建一个带有dev Node.js包的应用程序映像。

为了让它工作,你可以这样调用:

# docker build --build-arg APP_ENV=production -t app-node .

对于任何试图构建基于窗户的映像的人,您需要访问参数%%用于cmd。

# Dockerfile Windows
# ...
ARG SAMPLE_ARG
RUN if %SAMPLE_ARG% == hello_world ( `
echo hehe %SAMPLE_ARG% `
) else ( `
echo haha %SAMPLE_ARG% `
)
# ...

顺便说一句,ARG声明必须放在FROM之后,否则参数将不可用。