7天docker入门:第3天Dockerfile实战_Go和分布式IM的博客-程序员宅基地

技术标签: 容器  运维  docker  

引言

这是docker入门教程系列的第3篇,如果完成了前面2篇,我想你应该是初步学会使用Docker了:

如果没看,我建议你去看看,官方的教程,真的很好不枯燥。

那么接下来,你可能会考虑如何在项目中应用Docker,所以,我们今天主要是讲解如何编写Dockerfile以及一些实践技巧。

别人的学习经历

作者也是一边学Docker,一边记录。所以,我把我的学习经历分享更你,共勉,一起加油!

截止写本教程前,我完成了如下内容:

  • 《Docker技术入门与实战 第3版》看了60%,最主要是docker基础(容器、镜像、卷、网络)、dockerfile以及docker compose等
  • 实践完了官方的2个教程:getting-started特定语言指南(go)
  • 实践过程中根据:Docker中文文档 的目录,在脑海中建立了一个docker知识体系的认知,这个网站内容方面有些啰嗦且过时,参考意义更大。
    达成了初步的目标:
  • 为自己的开源分布式IM项目:Coffeechat 编写了3个Dockerfile并且成功运行
  • 每次手动启动多个Docker容器很麻烦,于是通过官方文档+查阅资料+调试实践花了半天,Docker compose的编排搞定了mysql,其他的redis还在研究中。
    在这期间,使用的编辑器有:
  • Visual Studio Code + Docker插件,其中可能是本机环境问题,compose文件没有智能补全,改为Goland编写compose文件
  • Goland + Docker插件
  • MacOS Big Sur,Docker destkop等

最后,啰嗦一下,不管是VS Code也好,Goland也好,选一款合适的IDE + Docker插件,会让我们少很多记忆负担~

Dockerfile回顾

完整内容请移步:特定语言指南(go)

一个简单的go项目

这是一个简单的Web项目(使用echo框架,但是我们并不需要关心框架细节),处理GET请求,返回一个“Hello Docker”。

源码只有一个main.go文件和2个go mod文件,内容如下:

$ git clone https://github.com/olliefr/docker-gs-ping
$ cd docker-gs-ping && vim main.go
package main

import (
    "net/http"
    "os"

    "github.com/labstack/echo/v4"
    "github.com/labstack/echo/v4/middleware"
)

func main() {
    
    e := echo.New()
    e.Use(middleware.Logger())
    e.Use(middleware.Recover())

    e.GET("/", func(c echo.Context) error {
    
        return c.HTML(http.StatusOK, "Hello, Docker! <3")
    })
    e.GET("/ping", func(c echo.Context) error {
    
        return c.JSON(http.StatusOK, struct{
     Status string }{
    Status: "OK"})
    })

    httpPort := os.Getenv("HTTP_PORT")
    if httpPort == "" {
    
        httpPort = "8080"
    }

    e.Logger.Fatal(e.Start(":" + httpPort))
}

执行后输出:

$ go run main.go
   ____    __
  / __/___/ /  ___
 / _// __/ _ \/ _ \
/___/\__/_//_/\___/ v4.2.2
High performance, minimalist Go web framework
https://echo.labstack.com
____________________________________O/_______
                                    O\
⇨ http server started on [::]:8080

此时,再启动一个终端,发送一个http请求:

$ curl http://localhost:8080/
Hello, Docker! <3

基础Dockerfile

针对上述简单的Go项目,很容易编写一个Dockerfile将其改造成Docker项目:

# syntax=docker/dockerfile:1

FROM golang:1.16-alpine

WORKDIR /app

# 拷贝2个go mod文件
COPY go.mod ./
COPY go.sum ./
RUN go mod download

# 拷贝main.go源文件
COPY *.go ./

# 编译,-o:指定编译的程序名
RUN go build -o /docker-gs-ping

EXPOSE 8080

CMD [ "/docker-gs-ping” ]

简单的解释一下:

  • FROM:必须得在第一行,指明继承的基础镜像,就想面向对象编程基础一个类一样。如果要自己实现基础镜像,可以基于scratch惊喜,它的大小几乎为0。
  • WORKDIR:工作目录,后面所有的相对路径都是基于这里。
  • COPY:拷贝文件到docker守护进程,以进行编码编译
  • RUN:执行命令,可以理解为bash
  • EXPOSE:端口映射,Docker容器和虚拟机一样,默认情况下宿主机和虚拟机网络是不通的。通过这个命令,我们就可以通过宿主机访问这个端口了。
  • CMD:运行Docker容器时执行的命令。Dockerfile有2个阶段,编译和运行。这个命令只在运行容器时执行,这一点初学者要注意,否则会比较懵。

然后,我们通过如下命令把它编译成docker镜像(注意,这里的镜像并不是类似.iso等一个大文件,在docker中是只一系列文件层的集合,当然可以导出为一个类似iso包含所有层的镜像文件):

# 编译镜像
$ docker build --tag docker-gs-ping .
# 列出本地镜像
$ docker image ls
REPOSITORY     TAG    IMAGE ID     CREATED SIZE 
docker-gs-ping latest 336a3f164d0f 43 minutes ago 540MB
# 启动
$ docker run -p 8080:8080 docker-gs-ping

多阶段构建

我们看到构建后的镜像有540MB,其实程序就一个文件,大多数都是编译环境之类的占了空间。这个时候我们可以分开编译和运行。

创建一个Dockerfile.multistage:

# syntax=docker/dockerfile:1

##
## Build
##
FROM golang:1.16-buster AS build

WORKDIR /app

COPY go.mod ./
COPY go.sum ./
RUN go mod download

COPY *.go ./

RUN go build -o /docker-gs-ping

##
## Deploy
##
FROM gcr.io/distroless/base-debian10

WORKDIR /
COPY --from=build /docker-gs-ping /docker-gs-ping

EXPOSE 8080

USER nonroot:nonroot

ENTRYPOINT ["/docker-gs-ping"]

然后再编译:

# -f:指定dockerfile
# -t:即-tag。如果省略:后面的版本号,就是latest
$ docker build -t docker-gs-ping:multistage -f Dockerfile.multistage .
$ docker image ls
REPOSITORY     TAG        IMAGE ID     CREATED SIZE
docker-gs-ping multistage e3fdde09f172 About a minute ago 27.1MB
docker-gs-ping latest     336a3f164d0f About an hour ago 540MB

我们看到,此时的镜像只有27MB。这个时候,可能你还会很疑惑,为什么还有这么大?能不能再小一点?

我们只需要更改Deploy阶段基础的镜像即可,选择一个更小的,比如alpine,以下是一个常用基础镜像的对比:
在这里插入图片描述

项目实战

Go Project layout介绍

根据官方的仓库:Github 翻译 ,我们实际的Go项目可能会有以下结构:
在这里插入图片描述

  • cmd:这个目录下,放程序的入口,即编译为可执行文件。
  • internal:该目录下放程序的逻辑,这下面的都是内部包,不能被其他包访问。

那么,以上图为例,我们要写Dockerfile的时候,应该怎么写呢

这就是我实战的时候遇到的问题,所以,我们先看看我的项目结构。

CoffeeChat项目结构简介

参考官方的layout后,我的项目结构如下:

├── README.md
├── api
├── app
│   ├── daemon
│   ├── demo
│   ├── im_filegw    # 文件网关
│   ├── im_gate      # tcp官网
│   ├── im_http      # http api
│   └── im_logic     # 程序逻辑,提供gRPC给im_gate和im_http调用 
├── go.mod
├── go.sum
├── internal
│   ├── filegw
│   ├── gate
│   ├── httpd
│   └── logic
├── pkg
│   ├── db
│   ├── def
│   ├── helper
│   ├── logger
│   └── mq

一开始,我打算把Dockerfile放在根目录下,里面启动所有的服务,但实际实现的时候,我发现这个Dockerfile很难写。后面通过一些文章以及官方的 构建镜像最佳实践 ,我改正了该错误,确定了一个原则:一个Dockerfile一个程序

实践技巧

一个Dockerfile一个程序

所以,针对类似上面结构的项目,个人建议把Dockerfile放在程序下面,根目录放docker compose做容器编排。如下:

server
├── api
├── app
│   ├── im_gate
│   │   ├── Dockerfile           # 这里放Dockerfile,配置文件也放在这个目录,方便和compose结合
│   │   ├── gate-example.toml
│   │   └── gate.go
│   ├── im_http
│   │   ├── Dockerfile           # 每个程序一个
│   │   ├── http-example.toml
│   │   └── http.go
│   └── im_logic
│       ├── Dockerfile           # 每个程序一个
│       ├── logic-example.toml
│       └── logic.go
├── docker-compose.debug.yml
├── docker-compose.yml           # 项目根目录,放compose,作编排
├── go.mod
├── go.sum
├── internal
│   ├── filegw
│   ├── gate
│   ├── httpd
│   └── logic
├── pkg
└── setup
    └── mysql
        └── init

那么,这个时候,我们就要注意路径的问题了。以im_http举例,Dockerfile内容如下:

FROM golang:1.16-alpine as build
LABEL maintainer="xmcy0011<[email protected]>"
WORKDIR /go/src/coffeechat

# 把当前所有文件 拷贝到上面的工作目录下(包括配置文件)
COPY . .

# 设置go代理,加快拉包速度
RUN go env -w GOPROXY=https://goproxy.io && \
    cd app/im_http && \
    # 拉项目依赖
    go mod tidy && \
    # 编译程序
    go build

##
## deploy 
##

FROM alpine
# 指定日志存储卷,当前工作目录下的Log文件
VOLUME [ "log” ]
# 第一行的as build,build是一个名称,这里使用
COPY --from=build /go/src/coffeechat/app/im_http .
CMD ["./im_http"]

Dockerfile中我们copy的是当前目录,但是源码在Dockerfile的…/…/目录中。故编译的时候,需要指定上下文:

$ cd server # 代码根目录
# 通过-f指定dockerfile。.:指定编译上下文为当前目录
$ docker build -t im_http:v0.1 -f app/im_http/Dockerfile .

接下来,我们介绍一些其他的实践技巧。

.dockerignore忽略不必要的文件和目录

默认情况下我们是把整个工程的文件都发送过去。但有一些源码的文件,比如.idea、脚本等等是不需要发送的。

此时我们可以在根目录下创建一个.dockerignore文件,忽略这些除源码以外的文件或文件夹。规则和.gitignore一样。

.dockerignore内容如下:

.idea    # 一行代表一个规则。比如这个就是忽略当前目录下的.idea目录
shell    # 忽略shell目录
build.sh # 忽略build.sh文件

合并指令,减少层数,使镜像体积更小

dockerfile中的一行,就代表了镜像的一层,如果这一层对应的文件内容发送改变,那就会重新构建这一层,而其他层使用缓存层越多,镜像体积越大,所以对指令进行合并是很有需要的。

层和层之间,都是相对的WORKDIR路径。上一层执行RUN cd,下一行RUN还是以WorkDIR为准,不会进入到上一个命令cd到的目录

PS:根据实测,很尴尬的是,下面2个方式,生成的镜像大小一样。但还是建议合并指令,更不容易出错。

没有合并之前:

RUN go env -w GOPROXY=https://goproxy.io
RUN cd app/im_http && go mod tidy
RUN cd app/im_http && go build

合并之后(更简洁,换行使用 “空格" 加 “" ):

RUN go env -w GOPROXY=https://goproxy.io && \
    cd app/im_http && \
    go mod tidy && \
    go build

选择合适的基础镜像

这个是影响镜像大小一个很重要的点,即使是golang,不同的版本也有不同的tag,我们可以选择小的。

拿docker hub中的golang1.16镜像为例:
在这里插入图片描述
所以:

  • 编译镜像,go推荐使用基础镜像:golang:1.16-alpine
  • 运行go程序,测试环境推荐使用镜像:alpine

以下是一个基础镜像的对比:
在这里插入图片描述

多阶段构建

多阶段构建的目标,就是为了分离编译和部署为2个环境,从而减少最终镜像的体积。因为运行的时候,针对静态语言,是不需要源码和编译环境的。

这个官方的教程中已经更出了明确的示例,这里就不在阐述。只需要记住2点即可:

  • 至少2个FROM,分别指定编译的基础镜像和运行的基础镜像
  • CMD指令只会在容器启动的时候执行,千万别搞混了

卷容器比mount目录更方便,但是mac查看偏麻烦

持久化容器数据的时候,主要有2种方式:一是直接使用宿主机的目录,二是使用卷容器。

第一种方式比较简单,启动容器的时候,直接通过增加-v参数,然后设定宿主机路径和容器路径即可完成映射。

但是在开发阶段或者学习docker阶段,针对Go语言,我感觉卷容器更好

  • docker compose文件有时候需要调整,特别是在集成mysql镜像时,直接删除整个卷容器,然后执行初始化逻辑感觉很省事。
  • 更好管理。直接使用docker volume ls,就可以查看所有的卷容器。

但是,在mac下,即使通过 inspect 找到了卷容器的路径,然后要进去查看也是比较麻烦:

$ docker inspect server_cim_mysql_data
[
    {
    
        "CreatedAt": "2021-11-01T01:19:42Z",
        "Driver": "local",
        "Labels": {
    
            "com.docker.compose.project": "server",
            "com.docker.compose.version": "1.29.2",
            "com.docker.compose.volume": "cim_mysql_data"
        },
        "Mountpoint": "/var/lib/docker/volumes/server_cim_mysql_data/_data",
        "Name": "server_cim_mysql_data",
        "Options": null,
        "Scope": "local"
    }
]

Mountpoint 指定了实际的位置,但是macOS并没有这个目录。它是经过转换之后的一个路径。

网上找到的解释是:

Docker for Mac在Linux VM中运行docker引擎,而不是Mac OS,因此您无法在Mac OS文件系统中找到卷的挂载点。卷文件应该存在于该Linux VM的文件系统中。
但是,您可以通过屏幕登录Docker for Mac的VM:

$ screen ~/Library/Containers/com.docker.docker/Data/com.docker.driver.amd64-linux/tty  
#user: root
#password: xxxxxx
$ ls -ltrh /var/lib/docker/volumes
total 148
drwxr-xr-x    3 root     root          4096 May 16 13:20 04576d248c19b1210d47e94c8211493428cd3c3aa71dfe3fa0f4214589a6f875
drwxr-xr-x    3 root     root          4096 May 16 13:20 31af0f01492d8f7b832dad75e731b754302e84fbecfa7c654d7de10465bec204

一个好的编辑器+Docker插件,能让我们事倍功半

一开始,按照官方的教程,我使用VS Code+Docker插件编写Dockerfile,它可以直接编译镜像而不需要输入命令,然后在底部会显示构建结果。

好处是能降低记忆负担,让我们快速上手

坏处是我们不能手敲命令,总是感觉没有入门

所以在学官方教程的时候,我还是使用iTerm来键入命令操作docker。
在这里插入图片描述
在这里插入图片描述

另外一个,使用VS Code最主要的原因是:针对Dockerfile的智能补全功能。但是在后面学习docker compose的时候,它失效了(至少在我的Mac上),没有任何提示,可能是因为环境问题或者配置异常导致。所以,我就转而在Goland中安装docker插件尝试编写dockerfile和docker compose。我发现,虽然它更笨重,但是有时候是真的强大。

比如,我们可以点击”mysql:5.7”快速跳转镜像对应的docker hub地址,查看详情。也可以点击左侧打绿色箭头,决定是启动整个服务,还是单个服务。
在这里插入图片描述

然后,在底部的Services,可以看到容器输出的日志。以及对容器进行暂停重启等,感觉非常方便。
在这里插入图片描述

所以,有时候,一个好的IDE,能让我们更快更方便的学习或者掌握某个技术,点赞。

PS1:上图是Goland2021.2.4
PS2:vs code写dockerfile相比goland更方便,主要是在于镜像编译、管理。

问题

运行的容器列表中为什么没有我的容器?

docker ps可以列出所有正在运行中的容器,但是可能因为各种原因,你的容器在启动后就退出了,或者启动失败。这个时候,我们可以增加-a参数,列出所有的容器。

$ docker ps -a

如果要排查,启动失败的原因或者退出的原因,可以通过logs命令查看启动日志,看看是否有错误输出。

$ docker logs <container_id>

如何进入docker容器以查看实际目录结构?

有2种情况:

  • 容器已启动,想进入查看
  • 容器启动失败,想进入确定目录结果,调试dockerfile

第一种情况 可以通过exec命令交互式进入

$ docker exec -it <continaer_id> /bin/sh   # 注意,不能bash,有些基础镜像中没有该命令

效果就像你ssh到一个linux主机一样,退出同样也是输出exit即可。

第二种情况 建议把CMD直接改成/bin/sh,然后启动容器的时候,不要加-d(后台启动)参数。

$ vim dockerfile.yml
...
CMD [ “/bin/sh" ]

$ docker run -it <continaer_id> # 不要加-d,直接前台启动docker容器,容器启动后就进入了shell,此时就可以查看dockerfile是哪里出了问题

docker compose如何与dockerfile结合?

docker compose适用于在单机环境下的服务编排,通常是基于已有的镜像(上传到了Docker Hub),那么如何基于项目源码编译镜像,然后编排部署呢?这样的好处在于:可以使自己的开源项目很容易被部署,且任意下载源码的人,都能通过修改源码即时看到效果。

docker compose中,可以通过build配置来直接从Dockerfile编译。

services:
  im_http: # http 服务
    container_name: im_http
    build: # 指定从dockerfile编译
      context: .
      dockerfile: app/im_http/Dockerfile
    volumes: # 数据卷绑定
      - ./log:/log

none:none是什么?如何清除

通过docker image ls 查看镜像列表时,有些id是none的镜像,这种镜像叫做空悬镜像,通常是由于构建过程异常导致残存的image,占用空间,可以使用命令清理:

$ docker image prune

如果无法删除,根据提示检查停止的容器删除后,再手动强制删除(加-f选项)镜像即可。

关于作者

觉得还不错,关注一下作者的公号吧~
请添加图片描述

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/xmcy001122/article/details/123122890

智能推荐

linux下free命令详解_ggaofeng的博客-程序员宅基地

https://www.cnblogs.com/ultranms/p/9254160.htmlhttp://blog.is36.com/linux_free_command_for_memory/

探秘计算机视觉中的注意力机制_百度大脑的博客-程序员宅基地

点击左上方蓝字关注我们【飞桨开发者说】储泽栋,北京交通大学软件学院学生,曾获ICPC国际大学生程序设计竞赛亚洲区域赛铜牌,全国大学生服务外包大赛二等奖近年来,越来越多的工作专注于将注意力...

对称矩阵的基本性质_牛牛码特的博客-程序员宅基地_对称矩阵的性质

1.对于任何方形矩阵X,X+XT是对称矩阵。2.A为方形矩阵是A为对称矩阵的必要条件。3.对角矩阵都是对称矩阵。4.两个对称矩阵的积是对称矩阵,当且仅当两者的乘法可交换。两个实对称矩阵乘法可交换当且仅当两者的特征空间相同。5.用&lt;,&gt;表示RN上的内积。n×n的实矩阵A是对称的。6.任何方形矩阵X,如果它的元素属于一个特征值不为2的域(例如实数),可以用刚好一种方法写成一个对称矩阵和一个斜对称矩阵之和。7.每个实方形矩阵都可写作两个实对称矩阵的积,每个复方形矩阵都可写作两个复对称矩阵的

郭晓东的“系列博客,专辑”集锦_hherima的博客-程序员宅基地

基础知识:字符编码的奥秘【专辑】,浏览其中一篇:字符编码的奥秘utf-8, Unicode《深度探索C++对象模型》【系列笔记】——对象模型、存储形式;默认构造函数一定会构造么?《深入理解计算机系统》【系列笔记】虚拟存储器,malloc,垃圾回收《PNG文件格式》PNG文件格式分析 iOS平台知识:iOS开始学习【系列博客】Objective-c 语法,继承,protocol和delegate(iOS学习笔记,从零开始)Objective-c高效编程【专辑】,浏览其中一篇:iOS 多线程

栈的应用实例---中缀表达式求值_先绅的博客-程序员宅基地

1.中缀表达式求值实现类package edu.tcu.soft;import java.util.Stack;/** * 功能:中缀表达式直接求值 */public class NifixExpre { // 定义操作数栈和操作符栈 private Stack operateNum = new Stack(); private Stack operateChara =

LilyPad Arduino可穿戴技术和电子织物控制器板简介_weixin_30632899的博客-程序员宅基地

LilyPad Arduino可穿戴技术和电子织物控制器板简介第1章LilyPad Arduino概览作为本书的第一章,在这里将为读者介绍LilyPad Arduino相关的基础知识。例如,LilyPad Arduino是什么、它可以做什么。除此之外,还将介绍要完成后续学习需要预备的一些技能,例如缝纫基础和本书的写作思想。在读完本章之后,读者就可以成竹在胸地进行学习和创作了本文选自Ar...

随便推点

Maven 之 profile 学习_MrMoving的博客-程序员宅基地

前言:在开发过程中,我们的项目会存在不同的运行环境,比如开发环境、测试环境、生产环境,而我们的项目在不同的环境中,有的配置可能会不一样,比如数据源配置、日志文件配置、以及一些软件运行过程中的基本配置,那每次我们将软件部署到不同的环境时,都需要修改相应的配置文件,这样来回修改,很容易出错,而且浪费劳动力。profiles的作用:配置一组不同的profile,以实现根据环境参数或命令行参数,激活指...

centos7安装python3.7_Centos7安装Python3.7(兼容Python2.7)_weixin_39708822的博客-程序员宅基地

Centos7安装Python3.7(兼容Python2.7) Centos7下已自动安装Python2.7.5,but现在经常会出现Python2和Python3兼容使用的情况,所以我现在记录下安装过程。上一篇文章我写过Centos6.5下升级Python2.7的操作Centos下升级Python本次操作与上一篇有所相识,但更为简易,下面请跟我一起操作预准备由于Centos需要提前安装Sqlit...

Python就是牛,2行Python就能实现 "文本文件" 差异对比!_菜鸟学Python的博客-程序员宅基地

比如,我们在过去的某个时候写了一段代码。后来,我们由于业务需求,对代码做了部分改动。一段时间过去了,我们想不起来这段代码,究竟改动了哪里?此时,本文讲述的这个功能,很好的帮助我们解决了...

ER图之在线考试_littlehu321的博客-程序员宅基地_在线考试系统er图

<br /><br />                                         考试系统ER图:<br /> 

Unity3d之Shader编程:子着色器、通道与标签的写法 & 纹理混合_dingxian8326的博客-程序员宅基地

一、子着色器Unity中的每一个着色器都包含一个subshader的列表,当Unity需要显示一个网格时,它能发现使用的着色器,并提取第一个能运行在当前用户的显示卡上的子着色器。我们知道,子着色器定义了一个渲染通道的列表,并可选是否为所有通道初始化所需要的通用状态。子着色器的写法如下:Subshader{ [Tags] [CommonState] Passdef ...

推荐文章

热门文章

相关标签