【问题标题】:How to execute a shell command before the ENTRYPOINT via the dockerfile如何通过 dockerfile 在 ENTRYPOINT 之前执行 shell 命令
【发布时间】:2017-05-21 14:26:57
【问题描述】:

我的 nodejs 项目有以下文件

FROM node:boron

# Create app directory
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

# Install app dependencies
COPY package.json /usr/src/app/
RUN npm install



# Bundle app source
COPY . /usr/src/app

# Replace with env variable
RUN envsubs < fil1 > file2

EXPOSE 8080
CMD [ "npm", "start" ]

我使用 -e 标志运行 docker 容器,提供环境变量

但我没有看到替代品。当 env 变量可用时会执行 Run c 命令吗?

【问题讨论】:

  • 旁注:当你设置workdir时,下一个命令将相对于这个路径,所以你可以把COPY package.json /usr/src/app/改成COPY package.json package.json,把COPY . /usr/src/app改成COPY . .
  • 此外,不需要RUN mkdir -p /usr/src/app,因为如果它不存在,WORKDIR 会创建它。 docs.docker.com/engine/reference/builder/#/workdir
  • envsubs &lt; fil1 &gt; file2 是什么?
  • 替换文件1和文件2中变量的命令为输出文件

标签: javascript node.js docker environment-variables


【解决方案1】:

图像是不可变的

Dockerfile 定义了镜像的构建过程。一旦构建,图像是不可变的(无法更改)。运行时变量不会被烘焙到这个不可变的图像中。所以 Dockerfile 是解决这个问题的错误地方。

使用入口点脚本

您可能想要做的是用您自己的脚本覆盖默认的ENTRYPOINT,并让该脚本对环境变量执行一些操作。由于入口点脚本将在运行时(容器启动时)执行,因此这是收集环境变量并对其进行处理的正确时间。

首先,您需要调整 Dockerfile 以了解入口点脚本。虽然 Dockerfile 不直接参与处理环境变量,但它仍然需要了解该脚本,因为该脚本将被烘焙到您的镜像中。

Dockerfile:

COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

ENTRYPOINT ["/entrypoint.sh"]
CMD ["npm", "start"]

现在,编写一个入口点脚本,该脚本在命令运行之前完成所需的任何设置,最后,exec 命令本身。

entrypoint.sh:

#!/bin/sh

# Where $ENVSUBS is whatever command you are looking to run
$ENVSUBS < fil1 > file2

npm install

# This will exec the CMD from your Dockerfile, i.e. "npm start"
exec "$@"

在这里,我已将npm install 包括在内,因为您在 cmets 中询问过这个问题。我会注意到这将在每次运行时运行npm install。如果合适的话,很好,但我想指出它每次都会运行,这会给您的启动时间增加一些延迟。

现在重新构建您的映像,因此入口点脚本是其中的一部分。

在运行时使用环境变量

入口点脚本知道如何使用环境变量,但您仍然需要告诉 Docker 在运行时导入该变量。您可以使用 -e 标志到 docker run 来执行此操作。

docker run -e "ENVSUBS=$ENVSUBS" <image_name>

这里告诉 Docker 定义一个环境变量ENVSUBS,它分配的值是当前shell环境中$ENVSUBS的值。

入口点脚本的工作原理

我将对此进行详细说明,因为在 cmets 中,您似乎对它们如何组合在一起有些模糊。

当 Docker 启动一个容器时,它会在容器内执行一个(并且只有一个)命令。该命令变为 PID 1,就像典型 Linux 系统上的 initsystemd 一样。该进程负责运行容器所需的任何其他进程。

默认情况下,ENTRYPOINT/bin/sh -c。您可以在 Dockerfile 或 docker-compose.yml 中覆盖它,或者使用 docker 命令。

当容器启动时,Docker 运行入口点命令,并将命令 (CMD) 作为参数列表传递给它。之前,我们将自己的ENTRYPOINT 定义为/entrypoint.sh。这意味着在您的情况下,这是 Docker 启动时将在容器中执行的内容:

/entrypoint.sh npm start

因为["npm", "start"] 被定义为命令,所以它作为参数列表传递给入口点脚本。

因为我们使用-e 标志定义了一个环境变量,所以这个入口点脚本(及其子脚本)将可以访问该环境变量。

在入口点脚本的末尾,我们运行exec "$@"。因为$@ 扩展为传递给脚本的参数列表,所以它将运行

exec npm start

因为exec 将其参数作为命令运行,替换当前进程,当你完成后,npm start 成为你容器中的 PID 1。

为什么不能使用多个 CMD

在 cmets 中,您询问是否可以定义多个 CMD 条目来运行多个事物。

您只能定义一个ENTRYPOINT 和一个CMD。在构建过程中根本不使用这些。与RUNCOPY 不同,它们不会在构建期间执行。它们在构建后作为元数据项添加到图像中。

只有稍后,当图像作为容器运行时,这些元数据字段才会被读取,并用于启动容器。

如前所述,入口点是真正运行的,它通过CMD 作为参数列表传递。它们分开的部分原因是历史原因。在 Docker 的早期版本中,CMD 是唯一可用的选项,ENTRYPOINT 被固定为/bin/sh -c。但是由于这种情况,Docker 最终允许ENTRYPOINT 由用户定义。

【讨论】:

  • 谢谢!这是有道理的,如果在启动时需要多个命令,则应将其添加为脚本
  • @user_mda 一个容器只能运行一个命令。就像 Linux 系统运行一个命令(initsystemd),然后它负责启动其他一切。因为你只能运行一件事(一个入口点),如果你想做更多的事情,你必须使用这样的脚本来做一些事情,然后再分叉你想要运行的实际命令(在这种情况下,@ 987654359@).
  • @user_mda 默认情况下,ENTRYPOINT/bin/sh -c,您的 CMD 作为参数传递给它。我们在这里所做的只是使用脚本而不是/bin/sh -c
  • 谢谢你的解释,现在试试.. 顺便说一句,多个 CMD 指令是否可以达到目的?
  • @user_mda 不可以,每张图片最多只能有一个CMD 和一个ENTRYPOINT
【解决方案2】:

当 env 变量可用时会执行 Run c 命令吗?

使用-e 标志设置的环境变量在您run 容器时设置。

问题是,Dockerfile 是在容器 build 上读取的,因此 RUN 命令将了解这些环境变量。

在构建时设置环境变量的方法是在你的 Dockerfile 中添加 ENV 行。 (https://docs.docker.com/engine/reference/builder/#/environment-replacement)

所以你的 Dockerfile 可能是:

FROM node:latest

WORKDIR /src
ADD package.json .

ENV A YOLO

RUN echo "$A"

还有输出:

$ docker build .
Sending build context to Docker daemon  2.56 kB
Step 1 : FROM node:latest
 ---> f5eca816b45d
Step 2 : WORKDIR /src
 ---> Using cache
 ---> 4ede3b23756d
Step 3 : ADD package.json .
 ---> Using cache
 ---> a4671a30bfe4
Step 4 : ENV A YOLO
 ---> Running in 7c325474af3c
 ---> eeefe2c8bc47
Removing intermediate container 7c325474af3c
Step 5 : RUN echo "$A"
 ---> Running in 35e0d85d8ce2
YOLO
 ---> 78d5df7d2322

您在RUN 命令启动时的最后一行看到,容器知道设置了环境变量。

【讨论】:

  • 所以,我不知道环境变量的值,它是容器启动时由另一个进程设置的。我只想读取它并覆盖一个文件,有什么方法可以实现吗?
  • 容器运行后先读取env变量再运行entrypoint命令的正确方法是什么?
【解决方案3】:

对于以bash 作为默认入口点的图像,如果需要,我会这样做以允许自己在 shell 启动之前运行一些脚本:

FROM ubuntu
COPY init.sh /root/init.sh
RUN echo 'a=(${BEFORE_SHELL//:/ }); for c in ${a[@]}; do source $x; done' >> ~/.bashrc

如果你想在容器登录时获取脚本,你可以在环境变量BEFORE_SHELL 中传递它的路径。使用 docker-compose 的示例:

version: '3'
services:
  shell:
    build:
      context: .
    environment:
      BEFORE_SHELL: '/root/init.sh'

一些备注:

  • 如果 BEFORE_SHELL 没有设置,那么什么都不会发生(我们有默认行为)
  • 您可以传递容器中可用的任何脚本路径,包括已安装的路径
  • 脚本来源,因此脚本中定义的变量将在容器中可用
  • 可以传递多个脚本(使用: 分隔路径)

【讨论】:

  • 感谢您的策略。真的很喜欢设置 $BEFORE_SHELL 环境变量的想法。期望默认情况下会内置这样的东西,但既然没有,喜欢轻松构建它的想法。
【解决方案4】:

我有一个extremely stubborn container,它在启动时不会运行任何东西。这种技术效果很好,我花了一天时间才找到,因为所有其他可能的技术都失败了。

  • 运行docker inspect postgres 查找入口点脚本。在这种情况下,它是docker-entrypoint.sh。这可能因容器类型和 Docker 版本而异。
  • 在容器中打开一个shell,然后找到完整路径:find / -name docker-entrypoint.sh
  • 检查文件:cat /usr/local/bin/docker-entrypoint.sh

在 Dockerfile 中,使用 SED 插入第 2 行(使用 2i)。

# Insert into Dockerfile 
RUN sed -i '2iecho Run on startup as user `whoami`.' /usr/local/bin/docker-entrypoint.sh

在我的特殊情况下,Docker 在启动时运行了这个脚本两次:首先作为 root,然后作为用户 postgres。可以使用test只在root下运行命令。

【讨论】:

    猜你喜欢
    • 2023-04-01
    • 2021-03-15
    • 2011-08-20
    • 1970-01-01
    • 1970-01-01
    • 2020-06-24
    • 1970-01-01
    • 1970-01-01
    • 2014-02-20
    相关资源
    最近更新 更多