Docker的ARG、ENV和.env配置完整指南

帮忙了解 ENV、ARG 、 env_file 、 .env; 了解如何使用docker 构建时变量、 环境变量和docker-compose 模版

Docker

ENV 指令

无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

ARG 指令

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所设置的构建环境的环境变量,在将来容器运行时是不会存在这些环境变量的

docker-compose

分两种变量: 1. 替换docker-compose.yml 的变量, 2. 注入到容器里的环境变量

db:
  image: "postgres:${POSTGRES_VERSION}"
// 当docker-compose up 执行时,会在当前shell 环境查询POSTGRES_VERSION 变量,并替换

docker-compose 文件支持 $VARIABLE and ${VARIABLE} 两种方式

.env 文件

1. env 文件仅在使用docker-compose.yml 文件时的预处理步骤中使用。$变量被替换为同一目录中名为“.env”的文件中包含的值

$  cat .env
TAG=v1.5

$ cat docker-compose.yml
version: '3'
services: 
  web: 
      image: "webapp:${TAG}"

$ docker compose convert
version: '3'
services: 
  web: 
      image: "webapp:v1.5"

shell 里环境变量 优先 于.env 文件变量

$ export TAG=v2.0
$ docker compose convert
version: '3'
services: 
  web: 
      image: "webapp:v2.0"

docker-compose 设置 容器里的环境变量

在docker-compose.yml 设置 environment key, 效果和 docker run -e VARIABLE=VALUE ...

web:
  environment:
    - DEBUG=1

传递shell环境变量到容器

可以直接使用当前shell 环境变量到容器里,就是在设置 environment key 时不加value,如果没有在当前的 shell 中导出环境变量 DEBUG,compose file 中会把它解释为 null; 效果和下列一样

$ docker run -e DEBUG -e DEBUG2 ubuntu env | grep VAR     #env是命令 查询环境变量
DEBUG=value1
DEBUG2=value2
web:
  environment:
    - DEBUG
    - DEBUG2

–env_file 为容器设置多个环境变量

通过使用env_file 参数,可以指定env 文件路径, 这时构建时可以指定不同的文件

 $ docker-compose --env-file ./config/.env.dev up 

设置多个环境变量到容器,通过制定 env file 文件 , 如同 docker run –env-file=FILE ...

web:
  env_file:
    - web-variables.env

前端项目 标准配置

# syntax=docker/dockerfile:1
FROM node:12 AS build
WORKDIR /app
COPY package* yarn.lock ./
RUN yarn install
COPY public ./public
COPY src ./src
RUN yarn run build

FROM nginx:alpine
COPY --from=build /app/build   /usr/share/nginx/html

最佳实践

在 docker-compose.yml 文件所在的目录创建 test.sh 和 prod.sh,test.sh 的内容如下

test.sh:

#!/bin/bash 
# define env var default value. 
export IMAGETAG=web:v1 
export APPNAME=HelloWorld 
export AUTHOR=Nick Li 
export VERSION=1.0

prod.sh

#!/bin/bash 
# define env var default value. 
export IMAGETAG=webpord:v1 
export APPNAME=HelloWorldProd 
export AUTHOR=Nick Li 
export VERSION=1.0LTS

测试环境下 执行 $ source test.sh $ docker-compose config

生产环境下,执行下面的命令:$ source prod.sh $ docker-compose config

docker wordpress 部署/迁移要点

1 wordpress 容器部署

version: "3.8"
services:
  db:
    image: mysql:8.0
    command:
    - --default_authentication_plugin=mysql_native_password
    - --character-set-server=utf8mb4
    - --collation-server=utf8mb4_unicode_ci     
    volumes:
      - db_data:/var/lib/mysql
    ports:
      - "3308:3306"
    restart: always
    environment:
      MYSQL_ROOT_PASSWORD: 111
      MYSQL_DATABASE: wordpress
      MYSQL_USER: user1
      MYSQL_PASSWORD: password
  wordpress:
    depends_on:
      - db
    image: wordpress:latest
    ports:
      - "8000:80"
    restart: always
    environment:
      WORDPRESS_DB_HOST: db:3308
      WORDPRESS_DB_USER: user1
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_PASSWORD: password
    volumes:
      ["./html/:/var/www/html"]
  phpmyadmin:
    depends_on:
      - db
    image: phpmyadmin
    restart: always
    ports:
      - '8001:80'
    environment:
      PMA_HOST: db
      PMA_PORT: 3308
      MYSQL_ROOT_PASSWORD: 111
volumes:
  db_data:

2. 原有的wordpress 目录下 wp-content/themes/ /wp-content/plugins/ 内容复制到新的 挂载点对应目录下

3 数据库恢复

方式1

mysql -uroot -p wordpress  < /root/wordpress.sql # mysql 命令 导入数据库

方式2

mysql>  source C:\Users\T440P\Desktop\test.sql   #进入mysql 命令用source命令

数据库其他命令

mysql 账户添加数据库权限
 
 grant all privileges on *.* to 'username'@'%' with grant option;

 flush privileges;

4 修改数据库原有域名

5 外层nginx 转发配置

docker里的 wordpress 拿到访问的域名,然后用php拼接到静态资源地址里, 所以外层 nginx 要把 用户的host 传给 wordpress,如下

proxy_set_header Host $host; # $host: 用户访问服务器header有’HOST’字段,就是host字段,如果没有,就是 server name里的值; $proxy_host 是 根据proxy_pass 的值走的
proxy_set_header X-Forwarded-Proto $scheme; #向转发的服务表明使用,访问的协议,如https

docker 里 修改 mysql 8 对外端口

mysql8 docker 容器修改端口 需要到容器里 /etc/mysql/my.cnf 修改或添加端口号, 建议从容器里复制出来修改

1 先复制到host

sudo docker cp wordpress_db_1:/etc/mysql/my.cnf ./  #从容器复制到host

2 编辑 my.cnf 修改或添加端口号 如port:3307

3 放回容器

sudo docker cp ./my.cnf wordpress_db_1:/etc/mysql/ #放回容器

4 然后重启 docker restart 容器名

修改后的端口和暴露的端口一致就可以连接成功, 如 – port 3307:3307

docker volumes

volumes

volumes 有两种形式

1 宿主机路径(HOST:CONTAINER)

volumes:

 - ~/configs:/etc/configs/:ro #host:container

2 数据卷名称(VOLUME:CONTAINER)

匿名卷即没有显示指定名字的卷,实名卷具有名字,两种卷均存储在/var/lib/docker/volumes/区域,匿名卷的目录名是一串数字,如果在创建容器是添加了-rm参数,则匿名卷会随着容器的删除而删除

volumes:
 - /var/lib/mysql  #匿名
 - db:/var/lib/mysql #VOLUME:container

dockerfile 详解

docker 文档

Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

FROM 指定基础镜像

FROM 就是指定 基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。

除了选择现有镜像为基础镜像外,Docker 还存在一个特殊的镜像,名为 scratch。这个镜像是虚拟的概念,并不实际存在,它表示一个空白的镜像。

FROM scratch

不以任何系统为基础,直接将可执行文件复制进镜像的做法并不罕见,比如 swarmetcd。对于 Linux 下静态编译的程序来说,并不需要有操作系统提供运行时支持,所需的一切库都已经在可执行文件里了,因此直接 FROM scratch 会让镜像体积更加小巧。使用 Go 语言 开发的应用很多会使用这种方式来制作镜像,这也是为什么有人认为 Go 是特别适合容器微服务架构的语言的原因之一。

RUN 执行命令

RUN 指令是用来执行命令行命令的。

其格式有两种:

shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样

RUN echo 'Hello, Docker!' > /usr/share/nginx/html/index.html

exec 格式:RUN [“可执行文件”, “参数1”, “参数2”],这更像是函数调用中的格式

既然 RUN 就像 Shell 脚本一样可以执行命令,那么我们是否就可以像 Shell 脚本一样把每个命令对应一个 RUN 呢?比如这样:

FROM debian:stretch 
RUN apt-get update 
RUN apt-get install -y gcc libc6-dev make wget 
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" 
RUN mkdir -p /usr/src/redis 
RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 
RUN make -C /usr/src/redis 
RUN make -C /usr/src/redis install

Dockerfile 中每一个指令都会建立一层,RUN 也不例外,而上面的这种写法,创建了 7 层镜像。这是完全没有意义的。

Dockerfile 支持 Shell 类的行尾添加 \ 的命令换行方式。很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

构建镜像

Dockerfile 文件所在目录执行:

# docker build [选项] <上下文路径/URL/-> 
docker build -t nginx:v3 .

镜像构建上下文(Context)

docker build 命令最后有一个  . 表示当前目录,这是在指定 上下文路径

 docker build 命令构建镜像,其实并非在本地构建,而是在服务端,也就是 Docker 引擎中构建的,当构建的时候,用户会指定构建镜像上下文的路径,docker build 命令得知这个路径后,会将这个路径下的所有内容打包,然后上传给 Docker 引擎

如果在 Dockerfile 中这么写:

COPY ./package.json /app/

这并不是要复制执行 docker build 命令所在的目录下的 package.json,也不是复制 Dockerfile 所在目录下的 package.json,而是复制 上下文(context) 目录下的 package.json

一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。

那么为什么会有人误以为  .  是指定 Dockerfile 所在目录呢?这是因为在默认情况下,如果不额外指定 Dockerfile 的话,会将上下文目录下的名为 Dockerfile 的文件作为 Dockerfile。但是Dockerfile 并不要求必须位于上下文目录中,比如可以用 -f ../Dockerfile.php 参数指定某个文件作为 Dockerfile

其它 docker build 的用法

直接用 Git repo 进行构建

$ docker build https://github.com/twang2218/gitlab-ce-zh.git#:11.1

并且指定默认的 master 分支,构建目录为 /11.1/

用给定的 tar 压缩包构建

$ docker build http://server/context.tar.gz

如果所给出的 URL 不是个 Git repo,而是个 tar 压缩包,那么 Docker 引擎会下载这个包,并自动解压缩,以其作为上下文,开始构建。

COPY 复制文件

COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径>

和 RUN 指令一样,也有两种格式,一种类似于命令行,一种类似于函数调用。

<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

COPY hom* /mydir/ 
COPY hom?.txt /mydir/ 
COPY --chown=55:mygroup files* /mydir/ //选项来改变文件的所属用户及所属组

ADD 更高级的复制文件

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。不如直接使用 RUN 指令,然后使用 wget 或者 curl 工具下载,处理权限、解压缩、然后清理无用文件更合理。因此,这个功能其实并不实用,而且不推荐使用。

CMD 容器启动命令

CMD 指令的格式和 RUN 相似,也是两种格式:

  • shell 格式:CMD <命令>
  • exec 格式:CMD [“可执行文件”, “参数1”, “参数2″…]
  • 参数列表格式:CMD [“参数1”, “参数2″…]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

在运行时可以指定新的命令来替代镜像设置中的这个默认命令,比如,ubuntu 镜像默认的 CMD 是 /bin/bash,如果我们直接 docker run -it ubuntu 的话,会直接进入 bash。我们也可以在运行时指定运行别的命令,如 docker run -it ubuntu cat /etc/os-release。这就是用 cat /etc/os-release 命令替换了默认的 /bin/bash 命令了,输出了系统版本信息。

在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 “,而不要使用单引号。

如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:

CMD echo $HOME

在实际执行中,会将其变更为:

CMD [ "sh", "-c", "echo $HOME" ]

使用 service nginx start 命令,则是希望 upstart 来以后台守护进程形式启动 CMD service nginx start 会被理解为 CMD [ “sh”, “-c”, “service nginx start”], 因此主进程实际上是 sh。那么当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。

正确的做法:直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:

CMD ["nginx", "-g", "daemon off;"]

ENTRYPOINT 入口点 有两个场景

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式和 shell 格式。

当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT 指令,换句话说实际执行时,将变为:

<ENTRYPOINT> "<CMD>"

场景一:让镜像变成像命令一样使用

假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:

FROM ubuntu:18.04
RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
CMD [ "curl", "-s", "https://ip.cn" ]

假如我们使用 docker build -t myip . 来构建镜像,我们需要查询当前公网 IP,只需要执行:

$ docker run myip 
当前 IP:61.148.226.66 来自:北京市 联通

跟在镜像名后面的是 command运行时会替换 CMD 的默认值。因此这里的 -i 替换了原来的 CMD。那么如果我们希望加入 -i 这参数,我们就必须重新完整的输入这个命令:

$ docker run myip curl -s https://ip.cn -i

而使用 ENTRYPOINT 就可以解决这个问题

FROM ubuntu:18.04 
RUN apt-get update \ 
  && apt-get install -y curl \ 
  && rm -rf /var/lib/apt/lists/* 
ENTRYPOINT [ "curl", "-s", "https://ip.cn" ]

直接使用 docker run myip -i  //CMD 的内容将会作为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,因此会作为参数传给 curl

场景二:应用运行前的准备工作

启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。

这些准备工作是和容器 CMD 无关的,无论 CMD 为什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 ENTRYPOINT 中去执行,而这个脚本会将接到的参数(也就是 <CMD> )作为命令,在脚本最后执行

ENV 设置环境变量

两种格式

  • ENV <key> <value>
  • ENV <key1>=<value1> <key2>=<value2> …
ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet" 
RUN curl -SLO "https://nodejs.org/dist/v$VERSION/node-v$VERSION-linux-x64.tar.xz" \

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。可以看到,将来升级镜像构建版本的时候,只需要更新 7.2.0 即可,Dockerfile 构建维护变得更轻松了。

镜像与容器理解

镜像是静态的,镜像的每一层都只是可读的,而容器是动态的里面运行着我们指定的应用,容器里面的应用可能会新建一个文件,修改一个目录,这些操作所带来的改变并不会作用到镜像里面,因为镜像只是可读的。所以通过镜像创建容器就是在镜像上加一个可读写的层。下面的图引用自docker docs,ID上有些不同。

一个镜像可创建多个容器,每个容器都有各自的一个可读写层,这些层相互独立共享下面的镜像

docker docker-compose

1. 新建并启动容器 docker run -d -p 80:80 –name webserver nginx // -d 后台运行 -p 主机端口:容器端口
[cc]docker stop webserver
docker rm webserver[/cc]
启动已stop 的容器 docker container start
[cc]docker build -t nginx:v3 [/cc]. 创建镜像
导出容器
[cc]docker container ls -a[/cc] //列出
[cc]docker export 7691a814370e > ubuntu.tar[/cc] 导出容器快照到本地
导入, 从容器快照文件中再导入为镜像
[cc]cat ubuntu.tar | docker import – test/ubuntu:v1.0[/cc]
利用 .bashrc to remove orphaned images
[cc]dclean() {
processes=`docker ps -q -f status=exited`
if [ -n “$processes” ]; then
docker rm $processes
fi

images=`docker images -q -f dangling=true`
if [ -n “$images” ]; then
docker rmi $images
fi
}[/cc]
2. docker-compose
软升级
[cc]$ docker-compose stop wordpress[/cc]
停止容器运行

$ docker-compose rm wordpress
删除老旧的容器
更新你的 image

[cc]$ docker-compose start wordpress[/cc]
启动新容器
还比如
你的 nginx 更新了配置文件之后
[cc]docker-compose restart nginx[/cc]

硬升级 用这个
[cc]docker-compose stop[/cc]
up 启动的用 docker-compose down
升级你的程序
[cc]docker-compose build nginx[/cc]
启动
[cc]docker-compose up -d nginx mysql[/cc]

Laradock 构建项目 key

构建laravel 项目 要到 workplace 容器
composer create-project laravel/laravel my-cool-app “5.2.*”
父目录下生成一个项目, 要修改配置 .env 目录配置
we need to replace ../:/var/www with ../my-cool-app/:/var/www

Laradock 添加xdebug
修改workplace xdedug true 和ini 配置 建workplace 和php-frm