如何用Docker部署Go Web应用程序(附教程)

850 阅读11分钟

简介

虽然大多数Go应用程序都编译为单一的二进制文件,但Web应用程序也有模板、资产和配置文件;这些文件可能不同步,导致部署失败。

Docker可以让我们创建一个独立的镜像,其中包含我们的应用程序所需的一切。在本教程中,你将学习如何用Docker部署Go网络应用程序,以及Docker如何帮助你改善开发工作流程和部署过程。

目标

在本文结束时,你将:

  • 对Docker有一个基本的了解。
  • 了解Docker如何在开发Go应用程序时帮助你。
  • 了解如何为生产中的Go应用程序创建一个Docker容器,以及知道如何使用持续集成和交付(CI/CD)来自动构建Docker镜像。

前提条件

在本教程中,你将需要:

  • 在你的设备上安装Docker。
  • 一个免费的Docker Hub账户。
  • 一个Semaphore账户。

你可以在golang-mathapp仓库中找到本教程的所有代码。

TomFern / golang-mathapp

了解Docker

Docker帮助你为你的应用程序创建一个单一的可部署单元。这个单元,也被称为容器,拥有应用程序工作所需的一切。这包括代码(或二进制)、运行时、系统工具和库。

将所有的需求打包成一个单元,确保应用程序在任何地方部署都有一个相同的环境。它也有助于保持相同的开发和生产设置。

容器也消除了因文件不同步或因生产环境的细微差别而引起的一系列问题。

相对于虚拟机的优势

容器提供了与虚拟机类似的资源分配和隔离的好处。然而,相似性到此为止。

虚拟机需要自己的客户操作系统,而容器则共享主机操作系统的内核。这意味着容器更轻,需要的资源更少。从本质上讲,虚拟机是操作系统中的操作系统。另一方面,容器就像系统中的任何其他应用程序。基本上,容器比虚拟机需要更少的资源(内存、磁盘空间等),而且启动时间比虚拟机快得多。

开发过程中Docker的好处

在开发中使用Docker的一些好处包括:

  • 一个所有团队成员都使用的标准开发环境。
  • 集中更新依赖性,并在任何地方使用相同的容器。
  • 开发环境与生产环境完全一致,以及修复可能只在生产中出现的潜在问题。

为什么在Go Web应用程序中使用Docker?

大多数Go应用程序都是简单的二进制文件。这就产生了一个问题:为什么要在Go应用程序中使用Docker?在Go中使用Docker的一些原因包括:

  • 网络应用程序通常有模板和配置文件。Docker有助于使这些文件与二进制文件保持同步。
  • Docker确保开发和生产中的设置完全相同。有的时候,应用程序在开发中可以运行,但在生产中却不能。使用Docker可以让你不必担心这样的问题。
  • 机器、操作系统和安装的软件在一个大的团队中可能有很大的不同。Docker提供了一种机制来确保一致的开发设置。这使团队更有效率,并减少开发过程中的摩擦和可避免的问题。

创建一个简单的Go网络应用程序

在这篇文章中,我们将用Go语言创建一个简单的网络应用程序作为演示。这个应用程序,我们将称之为MathApp,将:

  • 为不同的数学运算提供路线。
  • 为视图使用HTML模板。
  • 使用一个配置文件来定制应用程序,以及对选定函数的测试

访问/sum/3/6 将会显示一个页面,上面有添加36 的结果。同样,访问/product/3/6 ,将显示一个包含36 的产物的页面。

在这篇文章中,我们使用了Beego框架。请注意,你可以为你的应用程序使用任何框架(或者根本不使用)。

最终的目录结构

完成后,MathApp的目录结构将是这样的。

MathApp
    ├── Dockerfile
    ├── Dockerfile.production
    └── src
        ├── conf
        │   └── app.conf    
        ├── go.mod    
        ├── go.src
        ├── main.go
        ├── main_test.go    
        ├── vendor
        └── views
            ├── invalid-route.html
            └── result.html

主应用程序文件是main.go ,位于src 目录下。这个文件包含了应用程序的所有功能。main.go 中的一些功能是用main_test.go 来测试的。

views 文件夹包含视图文件invalid-route.htmlresult.html 。配置文件app.conf 被放置在conf 文件夹中。Beego使用这个文件来定制应用程序。

创建GitHub存储库

我们将使用Go mod,官方的模块管理器,以一种可移植的方式处理Go模块,而不必担心GOPATH。

我们首先要创建一个GitHub仓库,并将其克隆到你的设备上。

使用存储库的名称来初始化项目。

$ mkdir src
$ cd src
$ export GOFLAGS=-mod=vendor
$ export GO111MODULE=on
$ go mod init github.com/YOUR_GITHUB_USER/YOUR_REPOSITORY_NAME 
# (example: go mod init github.com/tomfern/go-web-docker)

从现在开始,我们可以使用这些命令。

$ go mod download
$ go mod vendor
$ go mod verify

vendor/ 文件夹中下载所需的依赖项(而不是在GOROOT 中下载,这在后面会很方便)。

应用程序文件内容

在继续之前,让我们创建文件结构。

$ mkdir conf views

主应用程序文件 (main.go) 包含所有的应用程序逻辑。这个文件的内容如下。

// main.go

package main

import (
    "strconv"
    
    "github.com/astaxie/beego"
)


func main() {
    /* This would match routes like the following:
       /sum/3/5
       /product/6/23
       ...
    */
    beego.Router("/:operation/:num1:int/:num2:int", &mainController{})
    beego.Run()
}

type mainController struct {
    beego.Controller
}


func (c *mainController) Get() {

    //Obtain the values of the route parameters defined in the route above    
    operation := c.Ctx.Input.Param(":operation")
    num1, _ := strconv.Atoi(c.Ctx.Input.Param(":num1"))
    num2, _ := strconv.Atoi(c.Ctx.Input.Param(":num2"))

    //Set the values for use in the template
    c.Data["operation"] = operation
    c.Data["num1"] = num1
    c.Data["num2"] = num2
    c.TplName = "result.html"

    // Perform the calculation depending on the 'operation' route parameter
    switch operation {
    case "sum":
        c.Data["result"] = add(num1, num2)
    case "product":
        c.Data["result"] = multiply(num1, num2)
    default:
        c.TplName = "invalid-route.html"
    }
}

func add(n1, n2 int) int {
    return n1 + n2
}

func multiply(n1, n2 int) int {
    return n1 * n2
}

在你的应用程序中,这可能被分割成几个文件。然而,为了本教程的目的,我喜欢把所有东西放在一个地方。

测试文件内容

main.go 文件中有一些需要测试的功能。这些功能的测试可以在main_test.go 。这个文件的内容如下:

// main_test.go

package main

import "testing"

func TestSum(t *testing.T) {
    if add(2, 5) != 7 {
        t.Fail()
    }
    if add(2, 100) != 102 {
        t.Fail()
    }
    if add(222, 100) != 322 {
        t.Fail()
    }
}

func TestProduct(t *testing.T) {
    if multiply(2, 5) != 10 {
        t.Fail()
    }
    if multiply(2, 100) != 200 {
        t.Fail()
    }
    if multiply(222, 3) != 666 {
        t.Fail()
    }
}

如果你想做持续部署,测试你的应用程序是特别有用的。如果你有足够的测试,那么你可以在任何时候,任何一天进行无压力的部署。

视图文件内容

视图文件是HTML模板;这些文件被应用程序用来显示对请求的响应。views/result.html 的内容如下。

<!-- views/result.html -->
<!doctype html>
<html>
    <head>
        <title>MathApp - {{.operation}}</title>
    </head>
    <body>
        The {{.operation}} of {{.num1}} and {{.num2}} is {{.result}}
    </body>
</html>

views/invalid-route.html 的内容如下。

<!-- invalid-route.html -->
<!doctype html>
<html>
    <head>
        <title>MathApp</title>
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <meta charset="UTF-8">
    </head>

    <body>
        Invalid operation
    </body>
</html>

配置文件内容

conf/app.conf 文件是由Beego读取的,用来配置应用程序。它的内容如下。

appname = mathapp
runmode = "dev"
httpport = 8010

在这个文件中。

  • appname:是应用程序运行的进程的名称。
  • httpport:是应用程序将被提供的端口,以及
  • runmode:指定应用程序应该以何种模式运行。有效值包括:dev ,用于开发;prod ,用于生产。

最后,如果你还没有这样做,请用以下方式安装Go模块。

$ go mod download
$ go mod vendor
$ go mod verify

在开发过程中使用Docker

本节将解释在开发过程中使用Docker的好处,并引导你完成在开发中使用Docker所需的步骤。

为开发配置Docker

我们将使用Dockerfile 来配置Docker进行开发。该设置应满足开发环境的以下要求。

  • 我们将使用上一节中提到的应用程序。
  • 文件应该可以从容器内部和外部访问。
  • 我们将使用bee 工具,这将在开发过程中用于实时重新加载应用程序(在Docker容器内)。
  • Docker将在端口8010 公开应用程序。
  • 在Docker容器中,应用程序位于/home/app
  • 我们为开发而创建的Docker镜像的名称将是mathapp.

步骤1 - 创建Docker文件

回到你项目的顶层。

$ cd ..

下面的Dockerfile应该满足上述要求。

FROM golang:1.18-bullseye

RUN go install github.com/beego/bee/v2@latest

ENV GO111MODULE=on
ENV GOFLAGS=-mod=vendor

ENV APP_HOME /go/src/mathapp
RUN mkdir -p "$APP_HOME"

WORKDIR "$APP_HOME"
EXPOSE 8010
CMD ["bee", "run"]

第一行。

FROM golang:1.18-bullseye

引用Go的官方镜像作为基础镜像。这个镜像已经预装了Go 1.18。

第二行。

RUN go install github.com/beego/bee/v2@latest

在全局范围内安装bee 工具(Docker命令默认以root身份运行),这将在开发过程中用于实时重载我们的代码。

接下来,我们为Go模块配置环境变量。

ENV GO111MODULE=on
ENV GOFLAGS=-mod=vendor

接下来的几行。

ENV APP_HOME /go/src/mathapp
RUN mkdir -p "$APP_HOME"
WORKDIR "$APP_HOME"

为代码创建一个文件夹,并使其处于活动状态。

接下来的最后一行告诉Docker,端口8010

EXPOSE 8010

最后一行。

CMD ["bee", "run"]

使用bee 命令来启动我们的应用程序。

第2步 - 构建镜像

一旦Docker文件被创建,运行以下命令来创建镜像。

$ docker build -t mathapp-development .

执行上述命令将创建一个名为mathapp 的镜像。

  • -t mathapp: 设置新镜像的标签名称,我们可以在以后引用该镜像为mathapp:latest
  • 不要忘记输入命令中的最后一个点(.),否则你会得到一个错误。

这个命令可以被每个从事这个应用程序的人使用。这将确保整个团队使用一个相同的开发环境。

要看到你系统上的图像列表,请运行以下命令。

$ docker images

注意,图像的确切名称和数量可能有所不同。然而,你应该在列表中看到至少有golangmathapp 镜像。

REPOSITORY               TAG            IMAGE ID            CREATED                 SIZE
golang                   1.18           25c4671a1478        2 weeks ago             809MB
mathapp-development      latest         8ae092824585        60 seconds ago          838MB

第3步 - 运行容器

一旦你有了mathapp ,你就可以用它来启动一个容器。

$ docker run -it --rm -p 8010:8010 -v $PWD/src:/go/src/mathapp mathapp-development

让我们来分析一下上述命令,看看它是做什么的。

  • docker run 命令是用来从一个镜像中运行一个容器的。
  • -it 标志在交互式模式下启动容器(将其与当前的shell绑定)。
  • --rm 标志在容器关闭后将其清理掉。
  • --name mathapp-instance 将容器命名为mathapp-instance
  • -p 8010:8010 标志允许通过端口8010 访问该容器。
  • -v $PWD/src:/go/src/mathapp 更为复杂。它将机器上的src/ 目录映射到容器中的/go/src/mathapp 。这使得开发文件在容器内外都可用,并且
  • mathapp 部分指定了要在容器中使用的镜像名称。

执行上述命令可以启动Docker容器。这个容器将你的应用程序暴露在端口8010 。每当你做出改变时,它也会自动重建你的应用程序。你应该在你的控制台看到以下输出。

______
| ___ \
| |_/ /  ___   ___
| ___ \ / _ \ / _ \
| |_/ /|  __/|  __/
\____/  \___| \___| v2.0.2
2022/05/10 13:39:29 INFO0003 Using 'mathapp' as 'appname'
2022/05/10 13:39:29 INFO0004 Initializing watcher...
2020/03/17 14:43:24.912 [I] [asm_amd64.s:1373]  http server Running on http://:8010

要检查设置情况,请在浏览器中访问http://localhost:8010/sum/4/5 。你应该看到类似于下面的内容。

**注意:**这假定你是在你的本地机器上工作。

要尝试实时重载功能,在任何一个源文件中进行修改。例如,编辑src/main.go ,替换这一行。

c.Data["operation"] = operation

改为类似这样的内容。

c.Data["operation"] = "real " + operation

Bee应该能接收到这个变化,即使是在容器内,也能无缝地重新加载应用程序。

______
| ___ \
| |_/ /  ___   ___
| ___ \ / _ \ / _ \
| |_/ /|  __/|  __/
\____/  \___| \___| v2.0.2
2022/05/10 13:39:29 INFO     ▶ 0003 Using 'mathapp' as 'appname'
2022/05/10 13:39:29 INFO     ▶ 0004 Initializing watcher...
2022/05/10 13:39:29 INFO.   [asm_amd64.s:1373]  http server Running on http://:8010

现在在浏览器上重新加载页面,以看到修改后的信息。

在生产中使用Docker

本节将解释如何在Docker容器中部署Go应用程序。我们将使用Semaphore来完成以下工作。

  • 在变化被推送到git仓库后自动构建
  • 自动运行测试
  • 如果构建成功并且测试通过,则创建一个Docker镜像,并且
  • 将Docker镜像推送到Docker Hub。

创建用于生产的Docker文件

我们将编写一个新的Dockerfile来创建一个完整的、独立的镜像;没有外部依赖。

在一个名为Dockerfile.production 的新文件中输入以下内容。

# Dockerfile.production

FROM registry.semaphoreci.com/golang:1.18 as builder

ENV APP_HOME /go/src/mathapp

WORKDIR "$APP_HOME"
COPY src/ .

RUN go mod download
RUN go mod verify
RUN go build -o mathapp

FROM registry.semaphoreci.com/golang:1.18

ENV APP_HOME /go/src/mathapp
RUN mkdir -p "$APP_HOME"
WORKDIR "$APP_HOME"

COPY src/conf/ conf/
COPY src/views/ views/
COPY --from=builder "$APP_HOME"/mathapp $APP_HOME

EXPOSE 8010
CMD ["./mathapp"]

让我们详细看看这些命令各自的作用。第一条命令。

FROM registry.semaphoreci.com/golang:1.18 as builder

告诉我们这是一个多阶段的构建;它定义了一个中间镜像,它只有一个任务:编译Go二进制文件。

你可能已经注意到,我们没有从Docker Hub(默认的镜像注册中心)获取镜像。相反,我们使用的是Semaphore Docker Registry,它更方便、更快捷,而且拉取的次数不会计入你的Docker Hub速率限制

下面的命令。

ENV APP_HOME /go/src/mathapp

WORKDIR "$APP_HOME"
COPY src/ .

创建应用程序的应用文件夹,并复制源代码。

中间镜像中的最后一条命令是下载模块并构建可执行文件。

RUN go mod download
RUN go mod verify
RUN go build -o mathapp

接下来是最终的和确定的容器,我们将在这里运行这些服务。

FROM registry.semaphoreci.com/golang:1.18

我们使用COPY 命令将文件复制到镜像中,--from 参数让我们从builder 阶段复制生成的二进制文件。

COPY src/conf/ conf/
COPY src/views/ views/
COPY --from=builder $APP_HOME/mathapp $APP_HOME

我们通过暴露端口和启动二进制文件来最终完成。

EXPOSE 8010
CMD ["./mathapp"]

要建立部署镜像。

$ docker build -t mathapp-production -f Dockerfile.production .

你可以用运行它。

$ docker run -it -p 8010:8010 mathapp-production

注意,我们不需要映射任何目录,因为所有的源文件都包含在容器中。

用Semaphore进行持续集成

Docker是打包和部署Go应用程序的绝佳解决方案。唯一的缺点是需要额外的步骤来构建和测试镜像。这个障碍最好用持续集成和持续交付(CI/CD)来解决。

持续集成(CI)平台可以在每次迭代、每次推送和每次合并中测试我们的代码。采用CI的开发者不必再担心合并分支,也不必对发布日感到焦虑。事实上,CI可以让开发者随时进行合并,并在一周内的任何一天进行安全发布。一个好的CI设置会运行一系列全面的测试,就像我们到目前为止准备的那些,以剔除任何bug。

一旦代码准备好了,我们可以用持续交付(CD)来扩展我们的CI设置。CD可以准备和构建Docker镜像,让它们随时可以部署。

推送代码到GitHub

让我们把我们的修改推送到GitHub。

  • 打开.gitignore ,取消对vendor/ 行的注释,这样就不会提交供应商的模块了。
# Dependency directories (remove the comment below to include it)
vendor/

# Build artifact
src/mathapp
  • 用git推送所有代码。
$ git add Dockerfile*
$ git add src
$ git add .gitignore
$ git commit -m "initial commit"
$ git push origin master

将仓库添加到Semaphore上

我们可以在短短几分钟内免费为我们的项目添加CI。

  • 进入Semaphore,使用Sign up with GitHub按钮进行注册。这将连接两个账户。
  • 点击项目旁边的**+(加号)**,创建一个新项目。

Create new project

  • 找到你的GitHub仓库,点击选择

Grab your repository

  • 选择Gostarter工作流程。先点击定制它

create a Dockerize Go workflow

你会得到工作流编辑器。下面是它的工作原理概述。

Semaphore workflow editor

  • 管线。一个管道有一个特定的目标,比如说建设或测试。管道是由块组成的,在代理中从左到右执行。
  • 代理。代理是为管道提供动力的虚拟机。我们有三种机器类型可供选择。该机器运行一个优化的Ubuntu 20.04图像,带有许多语言的构建工具。
  • 区块:区块将可以并行执行的作业分组。一个区块中的作业通常有类似的命令和配置。一旦一个区块中的所有工作完成,下一个区块就开始了。
  • 作业:作业定义了执行工作的命令。它们从其父块中继承其配置。

再来看看我们的设置。开始的工作流程希望代码在项目的根部,但我们的代码在src 目录内,所以我们需要做一个小小的修改。

  • 点击 "测试"块。
  • 在右边,你会发现工作的命令,改变它们,使它们看起来像这样。
sem-version go 1.18
export GO111MODULE=on
export GOPATH=~/go
export PATH=/home/semaphore/go/bin:$PATH
checkout
cd src
go get ./...
go test ./...
go build -v .

Go test commands

  • 点击 "运行工作流",然后点击 "开始",让流水线运行起来。

Switching GO version

如果一切顺利,几秒钟后,工作应该没有错误地完成。

加强CI管线

在本节中,我们将修改管道,以便。

  • 缓存Go的依赖性,以避免在每次运行时重新下载。
  • 测试有自己的块,因此我们可以更容易地扩展测试

要开始,请点击编辑工作流按钮,然后。

  1. 点击该块。我们将完全替换其内容。
  2. 将块的名称和工作改为 "安装"。
  3. 在作业命令框中输入以下内容。
sem-version go 1.18
export GO111MODULE=on
export GOPATH=~/go
export PATH=/home/semaphore/go/bin:$PATH
checkout
cd src
cache restore vendor-$SEMAPHORE_GIT_BRANCH-$(checksum go.mod),vendor-$SEMAPHORE_GIT_BRANCH,vendor-master
go mod vendor
cache store vendor-$SEMAPHORE_GIT_BRANCH-$(checksum go.mod),vendor-$SEMAPHORE_GIT_BRANCH,vendor-master vendor

Build code before building a Dockerize Go pipeline

我想这是一个学习Semaphore工具箱内置命令的好机会。

  • checkout:checkout命令克隆GitHub仓库的正确版本,并更改目录。 它通常是工作中的第一个命令。
  • sem-version:通过sem-version,我们可以切换一种语言的活动版本。Semaphore完全支持许多语言,包括Go
  • cache:cache是一个项目文件存储器。我们将使用缓存来持久化vendor/ 目录。

让我们回到我们的流水线。

  1. 使用**+添加块**的虚线按钮来创建一个新的块。
  2. 将该块和工作称为 "Test"。
  3. 打开环境变量部分,像我们在前一个块上做的那样,创建GO111MODULEGOFLAGS 变量。
  4. 打开 "序言"部分,该部分在区块中每个作业之前执行,并输入以下命令。
sem-version go 1.18
export GO111MODULE=on
export GOPATH=~/go
export PATH=/home/semaphore/go/bin:$PATH
checkout
cd src
cache restore vendor-$SEMAPHORE_GIT_BRANCH-$(checksum go.mod),vendor-$SEMAPHORE_GIT_BRANCH,vendor-master
  1. 在作业中键入以下命令。
go test ./...

Test code before building a Dockerize Go pipeline

  1. 点击运行工作流开始尝试更新的管道。

构建Docker镜像

到目前为止,我们所做的一切都进入了持续集成的范畴,自然的下一个阶段就是将应用程序打包到Docker容器中。

我们将创建一个新的交付管道来。

  • 用我们的Go二进制文件和HTML模板建立一个Docker镜像。
  • 将镜像上传到Docker Hub,这样它就可以进行部署了。

首先,我们要告诉Semaphore如何连接到Docker Hub。

  1. 在账户菜单上,点击设置

Settings menu

  1. 点击Secrets,然后点击Create New Secret

  2. 为你的Docker Hub用户名和密码创建两个变量。

    • DOCKER_USENAME =你的docker用户名

    • DOCKER_PASSWORD = 你的docker密码

Docker Hub secret to Dockerize Go

  1. 点击 "保存"。

回到流水线上。

  1. 点击编辑工作流程
  2. 使用**+Add First Promotion**按钮,创建一个新的链接管道。

Add a promotion

  1. 将管道的名称改为 "Dockerize"
  2. 勾选启用自动推广。你可以在这里设置条件来触发管道。

Dockerize Go promotion

  1. 点击**+添加区块**。我们将新的区块称为 "Build"
  2. 打开 "秘密"部分,勾选dockerhub方框。这将把我们之前创建的变量导入块中的作业中。

New block

  1. 在作业中输入以下命令。
checkout
echo "$DOCKER_PASSWORD" | docker login  --username "$DOCKER_USERNAME" --password-stdin
docker pull $DOCKER_USERNAME/mathapp-production:latest
docker build -f Dockerfile.production --cache-from $DOCKER_USERNAME/mathapp-production:latest -t $DOCKER_USERNAME/mathapp-production:latest .
docker push $DOCKER_USERNAME/mathapp-production:latest

Dockerize Go block

  1. 点击 "运行工作流"并开始
  2. 一旦前两个区块完成,点击推广按钮。

Dockerize Go promotion

等待几秒钟,直到Dockerize管道完成。

Dockerize Go application pipeline

检查你的Docker Hub仓库,你应该找到新的镜像,准备使用。

Dockerize Go application image

最后,在你的机器上拉出并测试新镜像。

$ docker pull YOUR_DOCKERHUB_USERNAME/mathapp-production
$ docker run -it -p 8010:8010 YOUR_DOCKERHUB_USERNAME/mathapp-production

下一步是什么

Docker为部署提供了多种可能性。

  • 自我托管:直接在一个虚拟机上运行镜像。通过一些脚本,我们可以将自动部署整合到你的CI/CD设置中。
  • PaaS:许多平台即服务的产品,如Heroku,可以直接运行Docker容器。欲了解更多细节,请查看以下链接。
  • Kubernetes:通过Kubernetes,我们可以大规模地运行应用程序。Kubernetes带来了很多功能,几乎所有的云供应商都支持。检查下面的链接,了解相关教程。

Heroku

Kubernetes

结论

在本教程中,我们学习了如何为Go应用程序创建一个Docker容器,并使用Semaphore准备Docker容器。

现在你应该已经准备好使用Docker来简化下一个Go应用程序的部署。如果你有任何问题,欢迎在下面的评论中发表。

P.S. 想持续提供用Docker制作的应用程序?请查看Semaphore的Docker支持

阅读下文。

The postHow To Deploy a Go Web Application with Dockerappeared first onSemaphore.