参考几篇非常优秀的文章:
不要轻易使用 Alpine 镜像来构建 Docker 镜像,有坑!
两个奇技淫巧,将 Docker 镜像体积减小 99%
听说你的 Docker 镜像比较胖?
Docker Compose 配置文件详解
以及非常优秀的开源项目:
Gin-Vue-Admin
Compose 是一个用于定义和运行多容器 Docker 应用程序的工具。借助 Compose,可以使用 YAML 文件来配置应用程序的服务。然后,使用单个命令,从配置中创建并启动所有服务。
基本上是一个三步过程:
1、使用 定义您的应用程序的环境,Dockerfile以便它可以在任何地方复制。
2、定义组成您的应用程序的服务,docker-compose.yml 以便它们可以在隔离的环境中一起运行。
3、运行docker compose up和Docker compose command启动并运行你的整个应用程序。您也可以docker-compose up使用 docker-compose 二进制文件运行。
常用命令:
docker-compose up:
使用docker-compose启动docker-compose.yaml中组合定义的所有容器
docker-compose up --build:
如果docker-compose.yaml中的某些镜像有改动,使用此命令重新打包镜像,并运行
docker-compose up -d:
效果与docker-compose up类似,但是由于增加了-d参数,会在后台启动
docker-compose up --build -d:
效果与docker-compose up --build类似,由于增加了-d参数,所有容器都会在后台运行。
docker system prune:
服务都启动成功后,使用此命令行可清除none镜像,如果有改变的镜像,重新打包后,会只更换这一个,这时这个镜像对应的原来old镜像会变更为none。
Compose 使用项目名称将环境彼此隔离。您可以在多个不同的上下文中使用此项目名称:
Compose 会保留您的服务使用的所有卷。当docker-compose up 运行时,如果发现任何集装箱从之前的运行,它会将从旧容器到新容器的体积。此过程可确保您在卷中创建的任何数据都不会丢失。
如果您docker-compose在 Windows 机器上使用,请参阅 环境变量并根据您的特定需求调整必要的环境变量。
Compose 缓存用于创建容器的配置。当您重新启动未更改的服务时,Compose 会重新使用现有容器。重用容器意味着您可以非常快速地更改环境。
我们在部署项目时,Mysql和Redis总不能每次都从新拉取镜像吧,这个功能就相当实用了,每次只会更新改变的镜像。
Compose 支持 Compose 文件中的变量。您可以使用这些变量为不同的环境或不同的用户自定义您的构图。有关更多详细信息,请参阅变量替换。
您可以使用该extends字段或通过创建多个 Compose 文件来扩展Compose 文件。有关更多详细信息,请参阅扩展。
由于目前还只有后端,所以暂时Dockerfile只有一个后端的,来看下Dockerfile文件:
FROM golang:alpine
WORKDIR /go/src/ETS/Server
COPY . .
RUN go generate && go env && go build -o Server .
FROM alpine:latest
LABEL MAINTAINER="[email protected]"
WORKDIR /go/src/ETS/Server
COPY --from=0 //go/src/ETS/Server ./
EXPOSE 8888
ENTRYPOINT ./Server -c config.docker.yaml
这里用到了多段构建,多阶段构建可以由多个 FROM 指令识别,每一个 FROM 语句表示一个新的构建阶段,阶段名称可以用 AS 参数指定
在声明构建阶段时,可以不必使用关键词 AS,最终阶段拷贝文件时可以直接使用序号表示之前的构建阶段(从零开始)。也就是说,下面两行是等效的:
COPY --from=example //go/src/ETS/Server ./
COPY --from=0 //go/src/ETS/Server ./
在这个Dockerfile文件中使用了两段构建,可以发现在两个阶段都存在着alpine字样,那么alpine究竟是什么呢。
对于那些耳熟能详的发行版(例如 Ubuntu、Debian、Fedora)来说,只能通过删除某些工具(例如 ifconfig 和 netstat)将镜像体积控制在 100M 以下。而对于 Alpine 而言,什么都不用删除,镜像大小也就只有 5M 而已。
Alpine 镜像的另一个优势是包管理工具的执行速度非常快,安装软件体验非常顺滑。诚然,在传统的虚拟机上不需要太关心软件包的安装速度,同一个包只需要装一次即可,无需不停重复安装。容器就不一样了,你可能会定期构建新镜像,也可能会在运行的容器中临时安装某些调试工具,如果软件包的安装速度很慢,会很快消磨掉我们的耐心。
其实我感觉现在整个build的过程就已经慢如蜗牛了,虽然我的镜像build完才65M。
其实仅在run阶段使用Alpine,也不是最佳的选择,因为我们使用Alpine的最重要原因就是其体积小,build出来的镜像整体体积都非常小,所以更多时候采用的都是全局使用Alpine。
Alpine使用的是动态库,可以根据自己的需要进行挂载,但是需要注意的是他的标准库使用的是musl libc,这个库相对于其他发行版的动态库glibc而言更小、更简单、更安全,然而二者也不兼容,所以想让程序跑在 Alpine 镜像中,必须在编译时使用 musl libc 作为动态库。
为了生成一个与 musl libc 链接的二进制文件,有两条路:
1、某些官方镜像提供了 Alpine 版本,可以直接拿来用。
2、还有些官方镜像没有提供 Alpine 版本,我们需要自己构建。
幸运的是golang官方就支持Alpine版本,在使用时,可以直接这样指定使用Alpine版本FROM golang:alpine。
但是其他官方不支持Alpine版本的怎么解决呢,例如c语言就没有gcc:alpine,这种情况就只能自力更生,从Alpine基础镜像自己组建环境,安装C的编译器。
但是这里还不能直接安装个gcc上去,因为gcc只是个编译器,没有提供依赖的各种基础库,这里可以安装build-base,相当于 Ubuntu 的 build-essentials,引入了编译器、标准库和 make 之类的工具。
可以以下面的小Dockerfile为例:
FROM alpine
RUN apk add build-base
COPY hello.c .
RUN gcc -o hello hello.c
FROM alpine
COPY --from=0 hello .
CMD ["./hello"]
可以看见使用RUN apk add命令进行软件的安装。
Alpine 提供了自己的包管理工具 apk,可以通过 https://pkgs.alpinelinux.org/packages 网站上查询包信息,也可以直接通过 apk 命令直接查询和安装各种软件。
Alpine官网
到这里应该了解了本文Dockerfile文件的大致流程,使用两个阶段构建,第一阶段使用golang:alpine镜像,进行项目编译工作,得到可运行于Alpine基础镜像的二进制可执行文件。
第二阶段将可执行文件复制到Alpine基础镜中,做出定制配置。
为每一个构建阶段指定WORKDIR,然后所有路径均使用绝对路径是一个好习惯。
可以看见第一阶段首先指定了WORKDIR路径为/go/src/ETS/Server,然后使用COPY 命令,这个命令会将Docker目录下所有的文件复制到golang:alpine镜像的WORKDIR所在路径内,然后执行RUN。
这里使用的&&是将几个命令一起执行,如果分开执行也是可以的,如下:
RUN go generate
RUN go env
RUN go build -o Server .
但是这样分开执行得到的镜像和&&链接执行得到的实际上是相同的,但是优点在于打包时层数变少,可以理解为,执行一个RUN打包的层数会增加一层,虽然减少层数并不能见效打包的体积,但是会获得更快的打包速度,速度也是重要的参考量。
到此完成了第一阶段的构建,得到了在基础镜像Alpine中可以运行的二进制文件。
在第二阶段中首先指定第二阶段的WORKDIR,保持与第一阶段相同,然后从第一阶段复制二进制文件,这里使用的是绝对路径,由于上下两个阶段都指定了WORKDIR,而且第二阶段复制时也是使用的绝对路径,这就不会造成一些复制时文件找不到,运行时路径错误等玄学问题。
是一个比较常用的手段。
最后使用EXPOSE 8888对外暴露端口8888.
在使用ENTRYPOINT ./Server -c config.docker.yaml设置环境变量的文件,如果不指定,程序运行自己内部的配置文件,这个文件可以保证每一次打包时拿到的都是初始化的配置,程序内部的配置文件会因为本地调试时的种种操作而存在各种各样的非初始化数据,造成程序异常。
最后再看一下最后的Dockerfile:
FROM golang:alpine
WORKDIR /go/src/ETS/Server
COPY . .
RUN go generate && go env && go build -o Server .
FROM alpine:latest
LABEL MAINTAINER="[email protected]"
WORKDIR /go/src/ETS/Server
COPY --from=0 //go/src/ETS/Server ./
EXPOSE 8888
ENTRYPOINT ./Server -c config.docker.yaml
1、为了打包镜像体积尽可能小,最优全局使用alpine基础镜像进行构建
2、为了打包速度尽可能快,第一阶段打包项目时,极可能的减少层数,利用&&链接命令
3、指定每一个阶段的WORKDIR,COPY时使用绝对路径(与WORKDIR相同)
先来看下完整的docker-compose.yml文件内容
version: "3"
networks:
network:
ipam:
driver: default
config:
- subnet: '177.7.0.0/16'
services:
server:
build:
context: ./Server
dockerfile: ./Dockerfile
container_name: ETS-Server
restart: always
ports:
- '8888:8888'
depends_on:
- mysql
- redis
links:
- mysql
- redis
volumes:
- /etc/timezone:/etc/timezone
- /etc/localtime:/etc/localtime
networks:
network:
ipv4_address: 177.7.0.12
mysql:
image: mysql:8.0.21
container_name: ETS-Mysql
command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci #设置utf8字符集
restart: always
ports:
- "3308:3306" # host物理直接映射端口为13306
environment:
MYSQL_DATABASE: 'ETS' # 初始化启动时要创建的数据库的名称
MYSQL_ROOT_PASSWORD: 'root' # root管理员用户密码
volumes:
- /etc/timezone:/etc/timezone
- /etc/localtime:/etc/localtime
networks:
network:
ipv4_address: 177.7.0.13
redis:
image: redis:6.0.6
container_name: ETS-Redis # 容器名
restart: always
ports:
- '16379:6379'
volumes:
- /etc/timezone:/etc/timezone
- /etc/localtime:/etc/localtime
networks:
network:
ipv4_address: 177.7.0.14
可以发现包含三个主要部分,即分别为version、networks、services
这部分是指定docker-compose使用的版本,具体的版本信息变更可以看官网:编写文件版本和升级
这里使用version: “3”
此项目使用了一个自定义的docker网络。
networks:
network:
ipam:
driver: default
config:
- subnet: '177.7.0.0/16'
由于我们在构建自己的服务时,常常会有很多不同的容器同时运行,彼此之间需要进行访问,例如你的服务需要使用数据库mysql,肯定需要进行连接。
因为有默认网络的存在,所以我们什么都不配置,在services下面的服务的容器里面都可以通过service名称访问其端口,前提是service需要通过ports发布它的端口出来。
通过docker-compose的默认网络就可以在宿主机上面访问服务的端口,但是docker-compose的默认网段是每次都会变化的。要想解决这个问题就只能通过在宿主机上面建立一个固定网段的网络,然后指定服务的网络为这个已经存在的网络,这样宿主机就可以通过这个网络的网关访问服务的端口了。
而且Docker-compose 默认 IP 子网与内部网络主机访问冲突也存在冲突的可能,所以更改Docker 子网也是常用的方法。
在文件中的networks字段下新建了一个network,这也是服务起来后字段ets_network中后面的network名,这里的网络服务名可以进行更换。
IPAM是配置部分的名称。IPAM是首字母缩写,代表IP地址管理。
driver是驱动部分的名称,这里使用了一个default默认网络。
config是网络配置吗这里划定了工作的网段,下面的services可以从这里面选取用来定义自己的网络。
即下面的services中Mysql、Redis、以及项目本身镜像都是在177.7.0.0/16之间选取设置ipv4_address。
本项目中services下暂时只有三个服务,这里以server为例。
server是services下的二级标签,可以由用户自定义,实际上这就是我们服务的名称
一些服务下面可能有image,即镜像名,指定当前这个服务的镜像名称或者镜像ID。如果镜像在本地不存在,Compose 将会尝试拉取这个镜像。
例如下面的格式都是正确的:
image: redis
image: ubuntu:14.04
image: tutum/influxdb
image: example-registry.com:4000/postgresql
image: a4bc65fd
服务除了可以基于指定的镜像,还可以基于一份 Dockerfile,在使用 up 启动之时执行构建任务,这个构建标签就是 build,它可以指定 Dockerfile 所在文件夹的路径。Compose 将会利用它自动构建这个镜像,然后使用这个镜像启动服务容器。
本项目的docker-compose就在这里使用了Server目录下的Dockerfile进行构建,然后得到构建镜像后再继续执行。
其中context是指定的上下文目录,可以是相对或者绝对路径。
dockerfile标签是指定build使用的Dockerfile文件。
如果你同时指定了 image 和 build 两个标签,那么 Compose 会构建镜像并且把镜像命名为 image 后面的那个名字。
既然可以在 docker-compose.yml 中定义构建任务,那么一定少不了 arg 这个标签,就像 Dockerfile 中的 ARG 指令,它可以在构建过程中指定环境变量,但是在构建成功后取消,在 docker-compose.yml 文件中也支持这样的写法:
build:
context: .
args:
buildno: 1
password: secret
下面这种写法也是支持的,一般来说下面的写法更适合阅读。
build:
context: .
args:
- buildno=1
- password=secret
这样构建过程可以向它们赋值。
注意:YAML 的布尔值(true, false, yes, no, on, off)必须要使用引号引起来(单引号、双引号均可),否则会当成字符串解析。
前面说过 Compose 的容器名称格式是:<项目名称><服务名称><序号>
虽然可以自定义项目名称、服务名称,但是如果你想完全控制容器的命名,可以使用container_name这个标签指定.
例如这里指定为了ETS-Server
在容器运行起来后可以查看容器名,一定是ETS-Server。
restart: "no" # no是默认的重启策略,在任何情况下都不会重启容器。
restart: always # 指定为always时,容器总是重新启动。
restart: on-failure # 如果退出代码指示出现故障错误,则on-failure将重新启动容器。
restart: unless-stopped
在3.7中已经被RESTART_POLICY取代。
3.7中出现的,用来取代restart,配置是否以及如何在容器退出时重新启动容器
ondition:一个none,on-failure或any(默认:) any。
delay:重新启动尝试之间等待的时间,指定为 持续时间(默认值:0)。
max_attempts:在放弃之前尝试重启容器的次数(默认值:永不放弃)。如果在配置中未成功重新启动 window,则此尝试不会计入配置的
max_attempts值。例如,如果max_attempts设置为“2”,并且第一次尝试时重新启动失败,则可能尝试重新启动两次以上。
window:在决定重启是否成功之前等待多长时间,指定为持续时间(默认值:立即决定)。
映射端口信息
宿主端口:容器端口 (即:HOST:CONTAINER) 的格式格式,或者仅仅指定容器的端口(宿主将会随机选择端口)都可以。
注意:当使用 HOST:CONTAINER 格式来映射端口时,如果你使用的容器端口小于 60 并且没放到引号里,可能会得到错误结果,因为 YAML 会自动解析 xx:yy 这种数字格式为 60 进制。为避免出现这种问题,建议数字串都采用引号包括起来的字符串格式
在本项目中使用的是3308:3306,可以理解为将主机的3308端口映射为容器的3306端口,即当访问主机的3308端口时,会将信息转发到容器的3306。
当然了这里使用3308:3308,也是可以的,但是Mysql内部默认开放的是端口3306,如果不进行设置的话,会一直无法连接。
2021-09-21T02:32:44.030063Z 0 [System] [MY-010931] [Server] /usr/sbin/mysqld: ready for connections. Version: '8.0.21' socket: '/var/run/mysqld/mysqld.sock' port: 3306 MySQL Community Server - GPL.
暴露端口,但不映射到宿主机,只被连接的服务访问
仅可以指定容器内部的端口为参数
expose:
- "3000"
- "8000"
解决容器的依赖、启动先后的问题
depends_on:
- db
- redis
实例中先启动容器 db 和 redis 再启动 web,注意:web 服务不会等待 redis 和 db 「完全启动」之后才启动
无论我们执行多少次这样的启动操作,这三个容器的启动顺序都是不变的。如果不应用 depends_on,每次执行 up 命令容器的启动顺序可能都是不一样的。
需要注意的是 depends_on 只是解决了控制容器启动顺序的问题,如果一个容器的启动时间非常长,后面的容器并不会等待它完成启动。如果要解决这类问题(等待容器完成启动并开始提供服务),需要使用 wait-for-it 等工具。
links:将指定容器连接到当前连接,可以设置别名,避免ip方式导致的容器重启动态改变的无法连接情况
links: # 指定服务名称:别名 - docker-compose-eureka-server:compose-eureka
links:
- mysql
- redis
这会让server连接mysql服务和redis服务
卷挂载路径
volumes:
- 本地的目录名称:容器中的目录名称
下面的实例是为了解决容器启动时,与本地时间区不一致,导致功能异常的解决方法
volumes:
- /etc/timezone:/etc/timezone
- /etc/localtime:/etc/localtime
这里说的networks是services的某一个服务的下级字段,是定义此服务的网络信息,例如这里是定义server的网络信息,设置其host信息,但是这里的ip字段要在顶级networks的区段内。
环境变量配置,可以用数组或字典两种方式
任何布尔值: true,false,yes,no需要用引号括起来,以确保它们不被YML解析器转换为True或False
如果服务指定了build选项,那么在构建过程中通过environment定义的环境变量将不会起作用。
将使用build的args子选项来定义构建时的环境变量。
覆盖容器启动后默认执行的命令
ommand: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci #设置utf8字符集
至此项目的docker-compose文件中用到的参数均已叙述完,也定义好项目的docker-compose文件了,下就是使用命令编译运行所有的容器就可以了。