nginx uri 参数对 .. 的解析

nginx uri 参数对 .. 的解析

在 Web 开发中,URI(Uniform Resource Identifier)是指用于标识某一互联网资源的名称。在 HTTP 协议中,URI 用于表示客户端请求的资源,如网页、图片等。Nginx 是一个高性能的 Web 服务器,能够处理海量的请求,因此对 URI 解析的性能要求也非常高。

Nginx 中的 URI 解析主要分为两个部分:请求行解析和 URI 解析。其中,请求行解析是指解析客户端请求的第一行,包括请求方法、URI 和协议版本;URI 解析则是对请求行中的 URI 进行进一步的解析和处理。
Nginx 中的 URI 解析主要包括 ngx_http_parse_request_line 和 ngx_http_parse_uri 两个函数。其中,ngx_http_parse_request_line 负责解析请求行,而 ngx_http_parse_uri 则负责解析 URI。

在解析 URI 时,需要考虑一些特殊情况,例如相对路径的引用和斜线的压缩等。具体来说,需要对以 “%XX” 形式编码的文本进行解码,解决对相对路径 “. “和”. .”的引用,以及可能将两个或多个相邻的斜线压缩为一个斜线之后,针对规范化的 URI 进行匹配。

需要注意的是,如果出现 http_host/../aaa/bb 的 uri,/../aa/bb 的第一个 “..” 导致了相对路径非法。在这种情况下,Nginx 打印出来的 uri 的值为空字符串。

下面是对一些常见 URI 进行解析的示例:

请求 uri
foo.com /
foo.com/ /
foo.com/aaa/bbb /aaa/bbb
foo.com/aaa/../bbb /bbb
foo.com/../aaa/bbb

nginx 中对 .. 的解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// ngx_http_parse_complex_uri 
case sw_dot_dot:

if (usual[ch >> 5] & (1U << (ch & 0x1f))) {
state = sw_usual;
*u++ = ch;
ch = *p++;
break;
}

switch (ch) {
#if (NGX_WIN32)
case '\\':
#endif
case '/':
state = sw_slash;
u -= 5;
for ( ;; ) {
if (u < r->uri.data) {
return NGX_HTTP_PARSE_INVALID_REQUEST;
}
if (*u == '/') {
u++;
break;
}
u--;
}
break;

当出现 .. 到小于 uri.data(uri 的起地址) 时 (在上面表格中的最后一个例子) , 返回 NGX_HTTP_PARSE_INVALID_REQUEST .

在 ngx_http_process_request_uri 里表现出来返回 NGX_HTTP_BAD_REQUEST

1
2
3
4
5
6
7
8
if (ngx_http_parse_complex_uri(r, cscf->merge_slashes) != NGX_OK) {
r->uri.len = 0;

ngx_log_error(NGX_LOG_INFO, r->connection->log, 0,
"client sent invalid request");
ngx_http_finalize_request(r, NGX_HTTP_BAD_REQUEST);//return 400
return NGX_ERROR;
}

openresty 变量和进程

变量的范围

在 OpenResty 里面,只有在 init_by_lua*
 和 init_worker_by_lua*
 阶段才能定义真正的全局变量。 这是因为其他阶段里面,OpenResty 会设置一个隔离的全局变量表,以免在处理过程污染了其他请求。

在 openresty 中共享变量的范围

  1. ngx.shared.DICT 用于整个 nginx 的共享
  2. ngx.var 或者是模块的变量,对应到每个单独的 lua VM ,只能在单独的 worker 进程共享。
  3. 使用数据库来做全局的共享,比如 memecached 、redis 、mysql 等。

init 阶段初始化的变量和 init_worker 阶段的有什么区别。

init 在 Nginx fork 工作进程之前运行,因此此处加载的数据或者代码将在所有工作进程中享受到操作系统提供的写复制(COW)特点,

共享内存

1
2
-- lua_shared_dict dict size;
lua_shared_dict foo 1m; # 定一个名字为 foo 的 1m 的共享内存

定时器

ngx.timer.at 在指定的时间后运行

1
2
3
4
5
6
7
8
ok, err = ngx.timer.at(delay, handler, ...) -- 启动定时器,delay 秒后执行handler函数

local function handler(premature, ...) -- 定时器的回调函数
if premature then -- 检查进程是否处于退出阶段
return -- 最适当的收尾工作
end
... -- 任意的 Lua 代码
end

timer 阶段执行的回调函数 handler 是与前台请求处理完全分离的,所以在函数里就不能使用 ngx.var、ngx.req、ngx.ctx 和 ngx.print 等请求相关的函数,但其他的大多数功能都是可用的,比如 cosocker、共享内存等,所以可以利用定时器绕过 init_by_lua、log_by_lua、header_filter_by_lua 等指令的限制,在这些阶段里 “间接” 使用 cosocket 访问后端。

在 openresty 中,有很多优秀的库都是使用了共享内存和 worker 变量之间的同步,根据不同的场景形成不同的策略

例如:

  • lua-resty-counter
  • lua-resty-event
  • lua-resty-lmcache

进程管理

OpenResty 的进程模式基于 Nginx,通常以 master/worker 多进程方式提供服务,各个 worker 进程相互平等且独立,由 master 进程通过信号管理各个 worker 进程。

在这个基础上 OpenResty 做了一些很有使用价值的改进,新增一个拥有 root 权限的特权进程,worker 进程也可以有自己的唯一标识,一定程度上能够实现服务的自我感知自我管理。

进程管理功能主要位于 ngx.process 库,它是 lua-resty-core 库的一部分,必须显式加载之后才能使用,即:

1
local process = require "ngx.process"   -- 显式加载 ngx.process 库

进程类型

OpenResty 里的进程分为如下六种类型:

  • single:单一进程,即非 master/worker 模式;
  • master:监控进程,即 master 进程;
  • signaller:信号进程,即 “-s” 参数时的进程;
  • worker:工作进程,最常用的进程,对外提供服务;
  • helper:辅助进程,不对外提供服务,例如 cache 进程;
  • privileged agent:特权进程,OpenResty 独有的进程类型。

当前代码所在的进程类型可以用函数 ngx.process.type 获取,例如:

1
2
3
local process require "ngx.process"        -- 显示加载 ngx.process 库
local str = process.type() -- 获取当前的进程类型
ngx.say("type is", str) -- 通常就是 "worker"

特权进程

特权进程是一种特殊的 worker 进程,权限与 master 进程一致(通常就是 root),拥有其他 worker 进程相同的数据和代码,但关闭了所有的监听端口,不对外提供服务,像是一个 “沉默的聋子”。

特权进程必须显式调用函数 ngx.process.enable_privileged_agent 才能启用。而且只能在 “init_by_lua” 阶段里运行,通常的形式是:

1
2
3
4
5
6
7
init_by_lua_block {
local process = require "ngx.process"
local ok, err = process.enable_privileged_agent() -- 启动特权进程
if not ok then -- 检查是否启动成功
ngx.log(ngx.ERR, "failed: ", err)
end
}

因为关闭了所有的监听端口,特权进程不能接受请求,“rewrite_by_lua” “access_by_lua” “content_by_lua” “log_by_lua” 等请求处理相关的执行阶段没有意义,这些阶段里的代码在特权进程里都不会运行。

但有一个阶段是它可以使用的,那就是 “init_worker_by_lua”,特权进程要做的工作就是 ngx.timer.* 启动若干个定时器,运行周期任务,通过共享内存等方式与其他 worker 进程通信,利用自己的 root 权限做其他 worker 进程想做而不能做的工作。

例如可以用 get_master_pid 获取 master 进程的 pid,然后在特权进程里调用系统命令 kill 发送 SIGHUP/SIGQUIˇ/SIGUSRl 等信号,实现服务的自我管理。

博客从 hexo 切换成 NotionNext

博客从 hexo 切换成 NotionNext

很久没有发布博客文章,原因是坐下来开始写的动作太复杂,使用 markdown 写作对图片的管理很麻烦

  • 需要一个专门的图片管理工具,虽然开源的 PicGo 已经很好用了
  • 需要将笔记内容转换成 markdown. markdown 的所见即所得其实并没有那么所见即所得.
  • 需要去发布,虽然只需要一行命令.

写博客最关键的是开始写.而使用 markdown 无时无刻不在想文档是怎么管理的,图片是怎么管理的,最后花在折腾各种工具上的时间远远大于写博客的时间.

当看到 NotionNext 的时候,就想起了当年使用 windows Live Writer(现在叫 https://openlivewriter.com/ ,已经被微软开源) 写博客的日子,不需要任何的多余动作:打开,写.

自从所有的笔记转移到 notion 后,笔记里面躺了很多的内容,但是一点都不想去整理到博客上面.我的博客也基本算是个公开的笔记,使用 Hexo 搭建的,有一段时间想着将 notion 里面的内容转到 Hexo 里面. 折腾了两天 notion2md .

bookmark

由于转出图片的问题,还给 notion2md 提了一个 merge.但是依然心有阻力.

在网上找相关的库,期望能够将 Hexo 的内容转换到 NotionNext . md2noton 基本可以满足需要.

于是开始照着例子写了一个 hexo 导入到 NotionNext datebase 的脚本. 写完后想着其他人可能也有这种需求,于是支持了指定配置文件、支持 hexo 格式的路径.

nobelium 相比 NotionNext 只相差一个 category 的列,所以也顺带支持了 nobelium .

link_preview

使用非常简单

  1. 安装 pip install hexo2notionnext
  2. 创建一个配置文件config.yaml 并填上配置
  3. hexo2notionnext -c config.yaml 开始导入

如何用 golang 生成比特币钱包

前一段时间看了区块链相关的内容,学习了一下bitcoin地址生成。内容来自网络。

第一步,随机选取一个32字节的数,大小介于1~0xFFFF FFFF FFFF FFFF FFFF FFFF FFFF FFFE BAAE DCE6 AF48 A03B BFD2 5E8C D036 4141之间,作为私钥

PS: 助记词是将这个随机数按每 11位分组映射到 2048 个单词位得到 12 16 或 24 个单词。

18e14a7b6a307f426a94f8114701e7c8e774e7f9a47e2c2035db29a206321725

第二步,使用椭圆曲线加密算法(ECDSA-SECP256k1)计算私钥所对应的非压缩公钥(共65字节,1字节0x04,32字节为x坐标,32字节为y坐标)。

0450863AD64A87AE8A2FE83C1AF1A8403CB53F53E486D8511DAD8A04887E5B23522CD470243453A299FA9E77237716103ABC11A1DF38855ED6F2EE187E9C582BA6

第三步,计算公钥的SHA-256哈希值

600FFE422B4E00731A59557A5CCA46CC183944191006324A447BDB2D98D4B408

第四步,计算上一步哈希值的RIPEMD-160哈希值

010966776006953D5567439E5E39F86A0D273BEE

第五步,在上一步结果之间加入地址版本号(如比特币主网版本号”0x00”)

00010966776006953D5567439E5E39F86A0D273BEE

第六步,计算上一步结果的SHA-256哈希值

445C7A8007A93D8733188288BB320A8FE2DEBD2AE1B47F0F50BC10BAE845C094

第七步,再次计算上一步结果的SHA-256哈希值

D61967F63C7DD183914A4AE452C9F6AD5D462CE3D277798075B107615C1A8A30

第八步,取上一步结果的前4个字节(8位十六进制数)D61967F6,把这4个字节加在第五步结果的后面,作为校验(这就是比特币地址的16进制形态)

00010966776006953D5567439E5E39F86A0D273BEED61967F6

第九步,用base58表示法变换一下地址(这就是最常见的比特币地址形态)

16UwLL9Risc3QfPqBUvKofHmBQ7wMtjvM

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
package main

import (
"bytes"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"fmt"
"golang.org/x/crypto/ripemd160"
"log"
)

const VERSION = byte(0x00)
const CHECKSUM_LENGTH = 4

type BitcoinKeys struct {
PrivateKey *ecdsa.PrivateKey
PublicKey []byte
}

func GetBitcoinKeys() *BitcoinKeys {
b := &BitcoinKeys{nil, nil}
b.newKeyPair()
return b
}

func (b *BitcoinKeys) newKeyPair() {
curve := elliptic.P256()
var err error
b.PrivateKey, err = ecdsa.GenerateKey(curve, rand.Reader)
if err != nil {
log.Panic(err)
}
b.PublicKey = append(b.PrivateKey.PublicKey.X.Bytes(), b.PrivateKey.PublicKey.Y.Bytes()...)
}

//获取地址
func (b *BitcoinKeys) GetAddress() []byte {
//1.ripemd160(sha256(publickey))
ripPubKey := GeneratePublicKeyHash(b.PublicKey)
//2.最前面添加一个字节的版本信息获得 versionPublickeyHash
versionPublickeyHash := append([]byte{VERSION}, ripPubKey[:]...)
//3.sha256(sha256(versionPublickeyHash)) 取最后四个字节的值
tailHash := CheckSumHash(versionPublickeyHash)
//4.拼接最终hash versionPublickeyHash + checksumHash
finalHash := append(versionPublickeyHash, tailHash...)
//进行base58加密
address := Base58Encode(finalHash)
return address
}

func GeneratePublicKeyHash(publicKey []byte) []byte {
sha256PubKey := sha256.Sum256(publicKey)
r := ripemd160.New()
r.Write(sha256PubKey[:])
ripPubKey := r.Sum(nil)
return ripPubKey
}

//通过地址获得公钥
func GetPublicKeyHashFromAddress(address string) []byte {
addressBytes := []byte(address)
fullHash := Base58Decode(addressBytes)
publicKeyHash := fullHash[1 : len(fullHash)-CHECKSUM_LENGTH]
return publicKeyHash
}

func CheckSumHash(versionPublickeyHash []byte) []byte {
versionPublickeyHashSha1 := sha256.Sum256(versionPublickeyHash)
versionPublickeyHashSha2 := sha256.Sum256(versionPublickeyHashSha1[:])
tailHash := versionPublickeyHashSha2[:CHECKSUM_LENGTH]
return tailHash
}

//检测比特币地址是否有效
func IsVaildBitcoinAddress(address string) bool {
adddressByte := []byte(address)
fullHash := Base58Decode(adddressByte)
if len(fullHash) != 25 {
return false
}
prefixHash := fullHash[:len(fullHash)-CHECKSUM_LENGTH]
tailHash := fullHash[len(fullHash)-CHECKSUM_LENGTH:]
tailHash2 := CheckSumHash(prefixHash)
if bytes.Compare(tailHash, tailHash2[:]) == 0 {
return true
} else {
return false
}
}

func main() {
keys := GetBitcoinKeys()
bitcoinAddress := keys.GetAddress()
fmt.Println("比特币地址:", string(bitcoinAddress))
fmt.Printf("比特币地址是否有效:%v\n:", IsVaildBitcoinAddress(string(bitcoinAddress)))
}


base58

Base58 采用数字、大写字母、小写字母,去除歧义字符 0(零)、O(大写字母 O)、I(大写字母i)、l(小写字母L),总计58个字符作为编码的字母表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
package main

import (
"bytes"
"math/big"
)

var b58Alphabet = []byte("123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz")

func Base58Encode(input []byte) []byte {
var result []byte

x := big.NewInt(0).SetBytes(input)

base := big.NewInt(int64(len(b58Alphabet)))
zero := big.NewInt(0)
mod := &big.Int{}

for x.Cmp(zero) != 0 {
x.DivMod(x, base, mod)
result = append(result, b58Alphabet[mod.Int64()])
}

ReverseBytes(result)

for _, b := range input {
if b == 0x00 {
result = append([]byte{b58Alphabet[0]}, result...)
} else {
break
}
}
return result

}

func Base58Decode(input []byte) []byte {
result := big.NewInt(0)
zeroBytes := 0
for _, b := range input {
if b != b58Alphabet[0] {
break
}
zeroBytes++
}
payload := input[zeroBytes:]
for _, b := range payload {
charIndex := bytes.IndexByte(b58Alphabet, b)
result.Mul(result, big.NewInt(int64(len(b58Alphabet))))
result.Add(result, big.NewInt(int64(charIndex)))
}

decoded := result.Bytes()
decoded = append(bytes.Repeat([]byte{byte(0x00)}, zeroBytes), decoded...)

return decoded
}

func ReverseBytes(data []byte) {
for i, j := 0, len(data)-1; i < j; i, j = i+1, j-1 {
data[i], data[j] = data[j], data[i]
}
}


2022年了,我还在怀念RSS

很久以前,我习惯用 Google Reader 来订阅各种有趣的独立博客来阅读。后来 Google Reader 关闭了,Google Reader 作为当时最好的 Rss 阅读器,可以快速的获取我感兴趣的内容。当 Google 关闭它后,我尝试了很多替代者,体验都 Google Reader 好。鲜果、抓虾、豆瓣 9 点 后来也关闭了。然后 开始用 inoreader 。

现在大家看视频用抖音,看新闻用头条这种用算法推荐的 app,可能 Rss 是什么已经不知道了。而微信公众打造的所谓的内容池,也只能在微信里面用关注的方式来订阅。

RSS仍然存在,但我怀念它,因为它不像以前那样随处可见了。

很久以前,每个网站都是封闭的。除了去那里,没有办法知道一个网站是否被更新。于是大家想出一种更容易聚合内容的方法,这就是 Rss ,大家都喜欢它并且用它来获取信息。

可能大家都不了解 Rss ,百科的解释是:

RSS(Really Simple Syndication)是一种描述和同步网站内容的格式,是使用最广泛的XML应用。RSS搭建了信息迅速传播的一个技术平台,使得每个人都成为潜在的信息提供者。发布一个RSS文件后,这个RSS Feed中包含的信息就能直接被其他站点调用,而且由于这些数据都是标准的XML格式,所以也能在其他的终端和服务中使用,是一种描述和同步网站内容的格式。

它曾经出现在每一个网络浏览器中,而且所有的主要网站,比如你看到的新闻网站,博客,都提供feeds。然后,你在你的电脑上安装一个feed阅读器,或者直接实用 Google Reader 这种网络的 Rss 阅读器。

这本身并没有什么特别的,但有了RSS,你也可以订阅任何提供feed的新闻网站或博客。这意味着你有一个一站式的服务。你有一个地方可以在你喜欢的网站更新时提醒你。

最流行的博客工具,如WordPress,仍然默认支持了Rss。由于很多的独立博客都是基于它来搭建的,所以很有可能你最喜欢的网站仍然提供RSS。

但是大的网络公司已经对 RSS 不再支持,他们在构建自己的护城河,比如微信公众号,你不能在那里深度的阅读任何的东西,只能蜻蜓点水的刷一刷。

可能是 10 年以前,我很喜欢用遨游浏览器,就是因为它可以很方便的订阅 Rss ,而现在很多浏览器取消的对 Rss 一键订阅的功能,好吧,这事情是 Chrome 干的。我认为是 Google 当时利用自己在搜索和浏览器的垄断地位杀死了 Rss 。

互联网之所以称之为互联网,是它的开放和便捷带来的。有人曾经说过,中国的互联网公司通用的互联互通的接口叫做爬虫。仔细想一想还真是那么回事。

Rss 真的是个非常妙的东西,用一个标准来提醒大家订阅更新了,我不需要去盯着它。也不需要为了获取特定的信息去打开一个特定的网站或者 App。 作为内容提供者,只需要提供好的内容就好。而现在大家都在制造标题党,都在发广告。

我在知乎上偶尔还会看到这样的问题: 202x 年什么 Rss 订阅工具比较好用?

很无奈,国内没有,不是因为做不出来,我看到了好几款订阅工具的出来,也都尝试过。最后都放弃了。不是他们不够好。而是国内内容生产已经都封闭起来了。你找不到好的内容。

也许还有有一些人和我一样,还坚持着使用着 Rss,但是很多年轻人不知道它出现过,只记得 App 里的那个关注按钮。

很可惜,大家只喜欢在社交平台上刷刷刷和手机屏幕里面飘过的老铁 666 。

很可惜,RSS 死了,我还怀念它。

使用 notion2md 从 notion 导出 markdown

为了用 notion 写公众号,我给 notion2md 提了个 merge 。

前段时间使用 notion 作为笔记,当需要将一篇笔记导出来贴到其他地方,比如公众号的时候,会有一些问题。主要碰到的是自带的一些图片导出的问题。

阅读更多

lua package 面向对象开发

lua 作为一门简单的语言,在变量是只有 table 和非 table 的区别。

基本

1
2
3
4
5
6
7
local _M = {} 
function _M:foo()
...
end

return _M

在 lua 中 _M:foo() 就是 _M.foo(self) 。在使用时和个人习惯有关。这里我希望能够严格的区分库和对象.当函数为静态方法时,比如一些utils函数,使用点号(.)。当函数需要面向对象时,使用冒号(:)。

阅读更多

计算最大公约数

这是计算最大公约数的函数.辗转相除法

1
2
3
4
5
6
function gcd(a, b)
if b == 0 then
return a
end
return gcd(b, a % b)
end

它的计算其实是使用欧拉定理.

https://zh.wikipedia.org/wiki/欧拉定理_(数论)

证明

对于任何可以整除a和b的整数,那么它也一定能整除a-b

1.

假设 a b 都有公约数 n 且 a>b, 假设 $a=x_1n$, $b=x_2n$

那么$ a-b =(x_1-x_2)n$

  1. a=kb+t 那么 t=a-kb

    $\frac{t}{d} = \frac{a}{d} -\frac{kb}{d}$

    因为 a、b都能够被d整除

    所以 $\frac{a}{d}-\frac{kb}{d}$ 为整数,

    即 $\frac{t}{d}$ 为整数,所以 d 也是 t 的公约数.

在均衡负载中,对设置的权重求最大公约数,需要用到 gcd 函数.

当传入的值是非数字时, 比如传入 字符串 “0”, b==0 判断失效

执行 gcd(”0”,nil) ,再次执行 gcd(nil,nil) 导致出现死循环.

https://mp.weixin.qq.com/s?__biz=Mzg3Njc0NTgwMg==&mid=2247487272&idx=1&sn=038a30ce61706c97e3397eee982b1486&amp

go-singlefilght

singleflight

1
2
3
golang.org/x/sync/singleflight


singleflight 是 go 提供的一个扩展并发原语,主要是用来合并请求来降低服务压力。

code

1
2
https://cs.opensource.google/go/x/sync/+/036812b2:singleflight/singleflight.go

原理

实现了一个 Group 的 struct

1
2
3
4
5
type Group struct {
mu sync.Mutex // protects m
m map[string]*call // lazily initialized
}

阅读更多

docker build alpine dns error

使用 alpine 构建 Ddockerfile ,导致 docker 打包失败,错误如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
fetch https://mirrors.aliyun.com/alpine/v3.13/main/x86_64/APKINDEX.tar.gz
fetch https://mirrors.aliyun.com/alpine/v3.13/community/x86_64/APKINDEX.tar.gz
v3.13.4-69-g5bcff43ec5 [https://mirrors.aliyun.com/alpine/v3.13/main]
v3.13.4-66-g32aee0eba0 [https://mirrors.aliyun.com/alpine/v3.13/community]
OK: 13892 distinct packages available
fetch https://mirrors.aliyun.com/alpine/v3.13/main/x86_64/APKINDEX.tar.gz
WARNING: Ignoring https://mirrors.aliyun.com/alpine/v3.13/main: DNS lookup error
fetch https://mirrors.aliyun.com/alpine/v3.13/community/x86_64/APKINDEX.tar.gz
WARNING: Ignoring https://mirrors.aliyun.com/alpine/v3.13/community: DNS lookup error
ERROR: unable to select packages:
bash (no such package):
required by: world[bash]
curl (no such package):
required by: world[curl]
ERROR: Service 'microservice-users-rpc' failed to build : The command '/bin/sh -c apk update && apk add --no-cache curl bash' returned a non-zero code: 2
make: *** [docker-compose-up] Error 1

阅读更多