第一部分 敏捷开发

人与人之间的交互是复杂的,并且其效果从来都难以预期,但却是工作中最为重要的方面。

第一章 敏捷实践

敏捷联盟宣言

  • 个体和交互胜过过程和工具
  • 可以工作的软件胜过面面俱到的文档
  • 客户合作胜过合同谈判
  • 响应变化胜过遵循计划

原则

  1. 我们最优先要做的是通过尽早的、持续的交付有价值的软件来使客户满意。
  2. 即使到了开发的后期,也欢迎改变需求。敏捷过程利用变化来为客户创造竞争优势。
  3. 经常性地交付可以工作的软件,交付的间隔可以从几周到几个月,交付的时间间隔越短越好。
  4. 在整个项目开发期间,业务人员和开发人员必须天天都在一起工作。
  5. 围绕被激励起来的个人来构建项目。给他们提供所需要的环境和支持,并且信任他们能够完成工作。
  6. 在团队内部,最具有效果并且富有效率的传递信息的方法,就是面对面的交谈。
  7. 工作的软件是首要的进度度量标准。
  8. 敏捷过程提倡可持续的开发速度。责任人、开发者和用户应该能够保持一个长期的、恒定的开发速度。
  9. 不断地关注优秀的技能和好的设计会增强敏捷能力。
  10. 简单 - 使未完成的工作最大化的艺术 - 是根本的。
  11. 最好的架构、需求和设计出自于自组织的团队。
  12. 每隔一段时间,团队会在如何才能更有效地工作方面进行反省,然后相应地对自己的行为进行调整。

第二章 极限编程概述

  • 客户作为团队成员
  • 用户素材
  • 短交付周期
    • 迭代计划
    • 发布计划
  • 验收测试
  • 结对编程
  • 驱动测试开发方法
  • 集体所有权
  • 持续集成
  • 可持续的开发速度
  • 开放的工作空间
  • 计划游戏
  • 简单的设计
    • 考虑能够工作的最简单的事情
    • 你将不需要它
    • 一次,并且只有一次
  • 重构
  • 隐喻

第三章 计划

当你能够度量你所说的,并且能够用数字去表达它时,就表示你了解了它;若你不能度量它,不能用数字去表达它,那么说明你的知识就是匮乏的、不能令人满意的。

第四章 测试

测试驱动的开发方法

还有一个更重要但是不那么明显的影响,是首先编写测试可以迫使我们使用不同的观察点。我们必须从程序调用者的有利视角去观察我们将要编写的程序。

此外,通过首先编写测试,我们就迫使自己把程序设计为可测试的。把程序设计为易于调用和可测试的,是非常重要的。为了成为易于调用和可测试的,程序
必须和它的周边环境解耦。这样,首先编写测试迫使我们解除软件中的耦合。

第五章 重构

重构就好比餐后对厨房的清理工作。第一次你没有清理它,你用餐是会快一点。但是由于没有对盘碟和用餐环境进行清洁,第二天做准备工作的时间就要更长
一点。这会再一次促使你放弃清洁工作。的确,如果跳过清洁工作,你今天总是能够很快用完餐,但是脏乱在一天天的积累。最终,你得花费大量的时间去寻找
合适的烹饪器具,凿去盘碟上已经干硬的食物残余,并把它们洗擦干净以使它们适合于烹饪。饭是天天要吃的。忽略掉清洁工作并不能真正加快做饭速度。

第六章 一次编程实践

代码仓库地址

第二部分 敏捷设计

拙劣设计的症状:

  • 僵化性: 设计难以改变
    • 很难对系统进行改动,因为每个改动都会迫使许多对系统其他部分的其他改动。
  • 脆弱性: 设计易于遭到破坏
    • 对系统的改动会导致系统中和改动的地方在概念上无关的许多地方出现问题。
  • 牢固性: 设计难以重用
    • 很难解开系统的纠结,使之成为一些可在其他系统中重用的组件。
  • 粘滞性: 难以做正确的事情
    • 做正确的事情比做错误的事情要困难。
  • 不必要的复杂性: 过分设计
    • 设计中包含有不具任何好处的基础结构。
  • 不必要的重复: 滥用鼠标
    • 设计中包含有重复的结构,而该重复的结构本可以使用单一的抽象进行统一。
  • 晦涩性: 混乱的表达
    • 很难阅读、理解。没有很好地表现出意图。

面向对象设计原则:

  • 单一职责原则 SRP
  • 开放-封闭原则 OCP
  • Liskov替换原则 LSP
  • 依赖倒置原则 DIP
  • 接口隔离原则 ISP

因此,简而言之,敏捷人员知道要做什么,是因为:

  1. 他们遵循敏捷实践去发现问题。
  2. 他们应用设计原则去诊断问题;并且
  3. 他们应用适当的设计模式去解决问题。

软件开发的这三个方面间的相互作用就是设计。

第七章 什么是敏捷设计

7.4 尽可能保持好的设计

敏捷开发人员致力于保持设计尽可能地适当、干净。这不是一个随便的或者暂时性的承诺。敏捷开发人员不是每几周才清洁他们的设计。而是每天、每小
时、 甚至每分钟都要保持软件尽可能地干净、简单并富有表现力。他们从不说,“稍后我会回来修正它。”他们决不让腐化出现。

7.5 结论

那么,什么是敏捷设计呢?敏捷设计是一个过程,不是一个事件。它是一个持续的应用原则、模式以及实践来改进软件的结构和可读性的过程。它致力于
保持系统设计在任何时间都尽可能得简单、干净以及富有表现力。

第八章 单一职责原则(SRP)

就一个类而言,应该仅有一个引起它变化的原因。

如果一个类承担的职责过多,就等于把这些责任耦合在了一起。一个职责的变化可能会削弱或者抑制这个类完成其他职责的能力。这种耦合会导致脆弱的
设计,当变化发生时,设计会遭到意想不到的破坏。

在SRP中,我们把指责定义为“变化的原因”。如果你能够想到多于一个的动机去改变一个类,那么这个类就具有多余一个的职责。有时,我们很难注意到
这一点。我们习惯于以组的形式去考虑职责。

在此还有一个推论。变化的轴线仅当变化实际发生时才具有真正的意义。如果没有征兆,那么去应用SRP,或者任何其他的原则都是不明智的。

第九章 开放 – 封闭原则(OCP)

软件实体(类、模块、函数等等)应该是可以扩展的,但是不可修改的。

如果程序中的一处改动就会产生连锁反应,导致一系列相关模块的改动,那么设计就具有僵化性的臭味。

遵循开放 – 封闭原则设计出的模块具有两个主要的特征。它们是:

  1. 对于扩展是开放的
    这意味着模块的行为是可以扩展的。当应用的需求改变时,我们可以对模块进行扩展,使其具有满足那些改变的新行为。换句话说,我们可以改变模块
    的功能。
  2. 对于更改是封闭的
    对模块行为进行扩展时,不必改动模块的源代码或者二进制代码。模块的二进制可执行版本,无论是可链接的库、DLL或者Java的.jar文件,都无需改动

一般而言,无论模块是多么的“封闭”,都会存在一些无法对之封闭的变化。没有对于所有情况都贴切的模型。

既然不可能完全封闭,那么就必须有策略地对待这个问题。也就是说,设计人员必须对于他设计的模块应该对哪种变化封闭做出选择。他必须先猜测出
最有可能发生变化的种类,然后构造抽象来隔离那些变化。

OCP背后的主要机制是抽象和多态。支持抽象和多态的关键机制之一是继承。正是使用了继承,我们才可以创建实现其基类中抽象方法的派生类。

是什么设计规则在支配着这种特殊的继承用法呢?
最佳的继承层次的特征又是什么呢?
怎样的情况会使我们创建的类层次结构掉进不符合OCP的陷阱中去呢?
这些正是Liskov替换原则要解答的问题。

第十章 Liskov替换原则(LSP)

子类型必须能够替换掉它们的基类型。

OCP是OOD中很多说法的核心。如果这个原则应用得有效,应用程序就会具有更多的可维护性、可重用性以及健壮性。LSP是使OCP成为可能的主要原则之一。
正是子类型的可替换性才使得使用基类类型的模块在无需修改的情况下就可以扩展。这种可替换性必须是开发人员可以隐式依赖的东西。因此,如果没有
显式地强制基类类型的契约,那么代码就必须良好地并明显地表达出这一点。

第十一章 依赖倒置原则(DIP)

  • 高层模块不应该依赖于低层模块。二者都应该依赖于抽象。
  • 抽象不应该依赖于细节。细节应该依赖于抽象。

一个稍微简单但仍然非常有效的对于DIP的解释,是这样一个简单的启发式规则:“依赖于抽象。”这是一个简单的陈述,该启发规则建议不应该依赖于具体
类————也就是说,程序中所有的依赖关系都应该终止于抽象类或者接口。

根据这个启发式规则,可知:

  • 任何变量都不应该持有一个指向具体类的指针或者引用
  • 任何类都不应该从具体类派生
  • 任何方法都不应该覆写它的任何基类中的已经实现了的方法

第十二章 接口隔离原则(ISP)

不应该强迫客户依赖于它们不用的方法。

这个原则用来处理“胖”接口所具有的缺点。如果类的接口不是内聚的,就表示该类具有“胖”的接口。换句话说,类的“胖”接口可以分解成多组方法。每一组
方法都服务与一组不同的客户程序。

ISP承认存在有一些对象,它们确实不需要内聚的接口;但是ISP建议客户程序不应该看到它们作为单一的类存在。相反,客户程序看到的应该是多个具有
内聚接口的抽象类。

第三部分 薪水支付案例研究

第十三章 COMMAND模式和ACTIVE OBJECT模式

事实上,该模式横过了一条非常有趣的界线。而这个交界处正是所有有趣的复杂性之所在。大多数类都是一组方法和相应的一组变量的结合。COMMAND模式
不是这样的。它只是封装了一个没有任何变量的函数。

另一个COMMAND模式的常见用法是创建和执行事务操作。

UNDO

如果command派生类的do()方法可以记住它所执行的操作的细节,那么undo方法就可以取消这些操作,并把系统恢复到原先的状态。

第十四章 template method模式和 strategy模式:继承与委托

像大多数美丽新世界一样,它最终也被证明有些不切实际。知道1995年,人们才清楚地认识到继承非常容易被过度使用,而且过度使用的代价是非常高的。
所以我们减少了对继承的使用,常常使用组合或者委托来代替它。

template method模式和 strategy模式都可以用来分离高层的算法和低层的具体实现细节。都允许高层的算法独立于它的具体实现细节重用。此外,
strategy模式也允许具体实现细节独立于高层的算法重用,不过要以一些额外的复杂性、内存 以及运行时间开销作为代价。

第十五章 facade模式和mediator模式

本章中论述的两个模式有着共同的目的。它们都把某种策略施加到另外一组对象上。facade模式从上面施加策略,而mediator模式则从下面施加策略。
facade模式的使用是明显且受限的,而mediator模式的使用则是不明显且不受限制的。

第十六章 singleton模式和monostate模式

singleton模式的好处

  • 跨平台:可以跨多个JVM和多个计算机工作
  • 适用于任何类:只需把一个类的构造函数变成私有的,并且在其中增加相应的静态函数和变量,就可以把这个类变为singleton
  • 可以透过派生创建:给定一个类,可以创建它的一个singleton子类
  • 延迟求值:如果singleton从未使用过,那么就决不会创建它。

singleton模式的代价

  • 摧毁方法未定义:没有好的方法去摧毁一个singleton,或者解除其职责
  • 不能继承:从singleton类派生出来的类并不是singleton。
  • 效率问题:每次调用instance方法都会执行if语句。就大多数调用而言,if语句是多余的。
  • 不透明性:singleton的使用者知道它们正在使用一个singleton,因为他们必须要调用instance方法。

monostate模式的好处

  • 透明性:使用monostate对象和使用常规对象没有什么区别。使用者不需要知道对象是monostate
  • 可派生性:monostate的派生类都是monostate。
  • 多态性:由于monostate的方法不是静态的,所以可以在派生类中覆写它们。

monostate模式的代价

  • 不可转换性:不能透过派生把常规类转换成monostate类
  • 效率问题:因为monostate是真正的对象,所以会导致许多的创建和摧毁开销。
  • 内存占用:即使从未使用monostate,它的变量也要占据内存空间。
  • 平台局限性:monostate不能跨多个JVM或者多个平台工作。

如果希望透过派生去约束一个现存类,并且不介意它的所有调用者都必须要调用instance()方法来获取访问权,那么singleton是最合适的。如果希望
类的单一性本质对使用者透明,或者希望使用单一对象的多态派生对象,那么monostate是最合适的。

第十七章 null object模式

可以使用null object模式来解决这些问题。通常,该模式会消除对null进行检查的需要,并且有助于简化代码。

那些长期使用C-based语言的人已经习惯于函数对某种失败返回null或者0。我们认为对这样的函数的返回值是需要检查的。null
object模式改变了这一
点。使用该模式,我们可以确保函数总是返回有效的对象,即使在它们失败时也是如此。这些代表失败的对象“什么也不做”。

第十八章 薪水支付案例研究:第一次迭代开始

数据库实现是细节!应该尽可能地推迟考虑数据库。有太多的应用程序之所以和数据库绑定在一起而无法分离,就是因为一开始设计时就把数据库考虑在内
了。请记住抽象的定义:本质部分的放大,无关紧要部分的去除。在项目的当前阶段数据库就是无关紧要的;它只不过是一项用来存储和访问数据的技术
而已。

代码仓库提交历史

第四部分

第二十一章 factory模式

依赖倒置原则告诉我们应该优先依赖于抽象类,而避免依赖于具体类。当这些具体类不稳定时,更应该如此。

factory模式允许我们只依赖抽象接口就能创建出具体对象的实例。

可替换的工厂

使用工厂的一个主要好处就是可以把工厂的一种实现替换为另一种实现。这样,就可以在应用程序中替换一系列相关的对象。

结论

工厂是有效的工具。在遵循DIP方面工厂有着重大的作用。它们使得高层策略模块在创建类的实例时无需依赖于这些类的具体实现。它们同样也使得在一组
类的完全不同系列的实现间进行交换成为可能。然而,使用工厂会带来复杂性,这种复杂性通常是可以避免的。缺省地使用它们通常不是最好的做法。

第五部分 气象站案例研究

第二十三章 composite模式

当然,使用composite模式并不能把所有的一对多关系都转变成一对一关系。只有那些以一致的方式对待列表中的每个对象的情况才具备转换的可能性。

尽管如此,还是有相当一部分一对多的关系适合于使用composite模式。并且好处还是相当大的。

第二十四章 observer模式 – 回归为模式

如果你熟悉设计模式,那么在面临一个设计问题时,你的脑海中很可能会浮现出一个模式。随后的问题就是直接实现这个模式呢,还是通过一系列小步骤
不断地去演化代码。

observer模式的最大推动力来自开放封闭原则。使用这个模式的动机就是为了在增加新的观察对象时可以无需更改被观察的对象。这样,被观察的对象就
可以保持封闭。

第二十五章 abstract server模式、adapter模式和bridge模式

这种情况是无法避免的。不存在完美的结构。只存在那些试图去平衡当前的代价和收益的结构。随着时间的过去,这些结构肯定会随着系统需求的改变而改
变。管理这种变化的诀窍是尽可能地保持系统简单、灵活。

使用adapter模式的解决方案是简单和直接的。它让所有的依赖关系都指向正确的方向,并且实现起来非常简单。bridge模式稍稍有些复杂。我建议在开始
时不要使用bridge模式,直到你明显可以看出需要完全分离连接策略和通信策略并且需要增加新的连接策略时,才使用这种方法。

第二十六章 proxy模式和 stairway to haven模式:管理第三方API

虽然代理会带来很多讨厌的问题,但是它们具有一个非常大的好处:重要关系的分离。

在那些把业务规则和数据库实现分离显得非常重要的情况中,proxy模式是很适用的。就此而言,proxy模式可以用来分离业务规则和任何种类的实现问题。
它可以用来防止业务规则被诸如:COM、CORBA、EJB等东西污染。这是当前流行的一种保持项目的业务规则逻辑和实现机制分离的方法。

远在真正需要proxy模式或者stairway to haven模式前,就去预测对于它们的需要是非常有诱惑力的。但这几乎从来都不是一个好主意,特别是对于
proxy模式。我建议在开始时先使用facade模式,然后在必要时进行重构。

第六部分 ETS案例研究

第二十八章 visitor模式

visitor模式的一个非常常见的应用是,遍历大量的数据结构并产生报表。这使得数据结构对象中不含有任何产生报表的代码。如果想增加报表,只需增加
新的访问者,而不需要更改数据结构中的代码。这意味着报表可以被放置在不同的组件中,并且仅被那些需要它们的客户单独使用。

一般来说,如果一个应用程序中存在有需要以多种不同方式进行解释的数据结构,就可以使用visitor模式。

在每个使用访问者的情况中,所使用的数据结构都独立于它的用途。可以创建新的访问者,可以更改现有的访问者,并且可以把所有的访问者重新部署到
安装地点而不会引起现有数据结构的重新编译和重新部署。这就是visitor模式的威力。

visitor模式系列给我们提供了许多无需更改一个层次结构中的类即可修改其行为的方法。因此,它们有助于我们保持OCP。此外,它们也提供了用来分离
不同种类的功能的机制,从而使类不会和很多其他的功能混杂在一起。它们也同样有助于我们保持CCP。也应该可以清楚地看出,visitor模式系列的结构
中也使用了SRP、LSP以及DIP。

visitor模式是有诱惑力的。在它们面前很容易会失去自制力。如果它们有用就去使用它们,但是请对它们的必要性保持健康的怀疑。通常,可以使用
visitor模式解决的问题往往也可以使用更简单的方法解决。

第二十九章 state模式

有限状态自动机是软件宝库中最有用的抽象之一。它们提供了一个简单、优雅的方法去揭示和定义复杂系统的行为。它们同样也提供了一个易于理解、易于
修改的有效实现策略。我在系统的各个层面,从控制高层逻辑的GUI到最低层的通讯协议,都会使用它们。它们几乎适用于任何地方。

开头

最近Google Analytics一直在催着升级到GA4,于是作为高级selfshosted OPS工程师当然需要将其部署到自己服务器上了。以下便是吾辈的快速启动配置。

traefik配置

traefik配置没有特殊之处,只是配置了自动tls。

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
version: "3.3"

services:
traefik:
container_name: traefik
image: traefik:latest
command:
- --providers.docker
- --log.level=INFO
- --accesslog=true
- --api.dashboard=true
- --entrypoints.web.address=:80
- --entrypoints.websecure.address=:443
- --certificatesresolvers.leresolver.acme.httpchallenge=true
- --certificatesresolvers.leresolver.acme.email=tangbin97@outlook.com
- --certificatesresolvers.leresolver.acme.storage=./acme.json
- --certificatesresolvers.leresolver.acme.httpchallenge.entrypoint=web
ports:
- 80:80
- 443:443
networks:
- traefik-public
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /root/acme/acme.json:/acme.json
labels:
- traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)
- traefik.http.routers.http-catchall.entrypoints=web
- traefik.http.routers.http-catchall.middlewares=redirect-to-https
- traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https
- traefik.http.routers.dashboard.rule=Host(`traefik.bilibifun.cn`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))
- traefik.http.routers.dashboard.service=api@internal
- traefik.http.routers.dashboard.entrypoints=websecure
- traefik.http.routers.dashboard.tls.certresolver=leresolver

environment:
- TZ=Asia/Shanghai

networks:
traefik-public:
external: true

plausible配置

plausible官方提供了docker compose文件,但是我们需要做一些改进:

  1. 首先我们是在Portainer中进行compose文件的编写,所以env_file配置就需要转换成environment
  2. clickhouse-server的额外配置看了一下只是禁用了一部分log所以先忽略这部分的自定义配置。
  3. 其余就是需要配置network了,我们需要将plausible本体连接到traefik网络中,但是内部service也需要互相访问,所以就需要两个network。所以我们就需要在plausible的labels配置中指定traefik的网络:- "traefik.docker.network=traefik-public"
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
version: "3.3"
services:
mail:
image: bytemark/smtp
networks:
- plausible-private

plausible_db:
image: postgres:14-alpine
volumes:
- db-data:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=postgres
networks:
- plausible-private

plausible_events_db:
image: clickhouse/clickhouse-server:22.6-alpine
volumes:
- event-data:/var/lib/clickhouse
networks:
- plausible-private
ulimits:
nofile:
soft: 262144
hard: 262144

plausible:
image: plausible/analytics:v1.5
command: sh -c "sleep 10 && /entrypoint.sh db createdb && /entrypoint.sh db migrate && /entrypoint.sh run"
depends_on:
- plausible_db
- plausible_events_db
- mail
networks:
- plausible-private
- traefik-public
labels:
- "com.centurylinklabs.watchtower.enable=false"
- "traefik.enable=true"
- "traefik.docker.network=traefik-public"
- "traefik.http.routers.plausible.rule=Host(`plausible.bilibifun.cn`)"
- "traefik.http.routers.plausible.entrypoints=websecure"
- "traefik.http.services.plausible.loadbalancer.server.port=8000"
- "traefik.http.routers.plausible.service=plausible"
- "traefik.http.routers.plausible.tls.certresolver=leresolver"
environment:
- DISABLE_REGISTRATION=true
- BASE_URL=replace me
- SECRET_KEY_BASE= replace me

volumes:
db-data:
driver: local
event-data:
driver: local
geoip:
driver: local

networks:
plausible-private:
traefik-public:
external: true

上文使用了dev.localhost域名来进行本地开发的https配置,这次我们换用自购域名解析到127.0.0.1的形式来进行配置。

首先我们需要购买一个域名并且添加解析到127.0.0.1:

因为我们的域名指向本地而不是公网可访问的IP,所以我们需要使用DNS challenge的形式来获取tls证书。各大厂商的DNS challenge的配置方式和需要的环境变量在这里:dnsChallenge 可以找到。下面使用alidns来进行配置:

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
version: "3.3"

services:
traefik:
container_name: traefik
image: "traefik:latest"
command:
- "--api.dashboard=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.websecure.address=:443"
- "--providers.docker"
- "--log.level=ERROR"
- "--certificatesresolvers.myresolver.acme.dnschallenge=true"
- "--certificatesresolvers.myresolver.acme.dnschallenge.provider=alidns"
# 检查之前延迟900s保证解析已经刷新
- "--certificatesresolvers.myresolver.acme.dnschallenge.delayBeforeCheck=900"
# 手动指定阿里DNS优化解析
- "--certificatesresolvers.myresolver.acme.dnschallenge.resolvers=223.5.5.5:53"
- "--certificatesresolvers.myresolver.acme.email=xxx@xxx.com"
- "--certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
networks:
- traefik-public
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "./letsencrypt:/letsencrypt"
labels:
- "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"
- "traefik.http.routers.http-catchall.entrypoints=web"
- "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
- "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
- "traefik.http.routers.dashboard.rule=Host(`traefik.bilibill.site`) && (PathPrefix(`/api`) || PathPrefix(`/dashboard`))"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls.certresolver=myresolver"
environment:
- "ALICLOUD_ACCESS_KEY=xxxxxxxxx"
- "ALICLOUD_SECRET_KEY=xxxxxxxxx"

portainer:
image: portainer/portainer-ce:latest
command: -H unix:///var/run/docker.sock
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
networks:
- traefik-public
labels:
# Frontend
- "traefik.enable=true"
- "traefik.http.routers.frontend.rule=Host(`portainer.bilibill.site`)"
- "traefik.http.routers.frontend.entrypoints=websecure"
- "traefik.http.services.frontend.loadbalancer.server.port=9000"
- "traefik.http.routers.frontend.service=frontend"
- "traefik.http.routers.frontend.tls.certresolver=myresolver"

# Edge
- "traefik.http.routers.edge.rule=Host(`edge.bilibill.site`)"
- "traefik.http.routers.edge.entrypoints=websecure"
- "traefik.http.services.edge.loadbalancer.server.port=8000"
- "traefik.http.routers.edge.service=edge"
- "traefik.http.routers.edge.tls.certresolver=myresolver"


volumes:
portainer_data:
networks:
traefik-public:
external: true

使用docker network create traefik-public创建外部网络后使用docker-compose up便可以启动服务,访问https://traefik.bilibill.site/dashboard/#/可以看到traefik自带的webUI。

0%