目标

采用一套开源组件,实现私有化的 CI/CD

流程

流程
流程
  1. 代码仓库 在 Repo 有 Commit 时触发事件,把 代码 推送到 Builder。本文选用 Gitlab。
  2. 镜像构建 把代码打包成 Docker image,并推送到 镜像仓库,再通知生产服务器。本文选用 Gitlab CI。
  3. 镜像仓库 储存 Docker Image。本文选用 Nexus。
  4. 生产服务器 接受通知后去镜像仓库拉取镜像,再删除老的 container,创建新的 container。本文借助 watchtower。

原则

  • 所有组件使用 Docker Compose 部署
  • 所有组件都使用 HTTPS,公网可访问
  • 所有组件使用验证,且使用强密码,未授权用户什么都做不了

本文规范

  • 所有配置都是真实可用的,部分配置项以占位符 ${} 的方式替代,使用时记得替换成自己的值
  • #EDIT 是需要注意的地方
  • 组件部署后,还需要登进去进行一些操作。但在写文的时候已经忘了,请发挥聪明才智 :)
  • watchtower 的 Docker-Compose、配置文件 放在 ~/docker-compose/watchtower 下,卷挂载在 ~/docker_volume/watchtower 下。其他组件同理。

占位符

1
2
3
4
5
${host} - 域名
${port} - 端口

${ALICLOUD_ACCESS_KEY}
${ALICLOUD_SECRET_KEY}

Traefik

Traefik 是一个网关,和 Nginx 比较类似,不过 Traefik 对 Docker 支持好。比如说,我只要 在 watchtower 的 Docker labels 上写好配置,访问 https://watchtower.${host}:${port} 就能自动路由到 watchtower container 的 80 端口上了。

首先做好域名解析,让阿里云域名 *.${host} 解析到自己的机子上。这里用了泛域名。

创建网络
docker network create traefik

~/docker-compose/traefik/docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
version: "3.9"
services:
reverse-proxy:
image: traefik
container_name: traefik
ports:
- "${port}:${port}"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./traefik.yml:/etc/traefik/traefik.yml
- ./dynamic_conf.yml:/root/dynamic_conf.yml
- ~/docker_volume/traefik/crt:/root/crt
- ~/docker_volume/traefik/log:/root/log
restart: unless-stopped
networks:
- traefik
environment:
ALICLOUD_ACCESS_KEY: ${ALICLOUD_ACCESS_KEY}
ALICLOUD_SECRET_KEY: ${ALICLOUD_SECRET_KEY}
networks:
traefik:
external: true

# docker network create traefik

~/docker-compose/traefik/dynamic_conf.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
http:
middlewares:
basic-auth:
basicAuth:
users:
- "admin:$2b$12$PASSPASSPASS" # EDIT VISIT https://doc.traefik.io/traefik/middlewares/basicauth/ !!!

routers:
dashboard:
rule: "Host(`traefik.`)"
service: "api@internal"
middlewares:
- basic-auth
services:
docker-hub-service:
loadBalancer:
servers:
- url: "http://nexus:8082"

~/docker-compose/traefik/traefik.yml

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
providers:
docker:
exposedByDefault: false
file:
filename: /root/dynamic_conf.yml
watch: true

api:
dashboard: true

entryPoints:
https:
address: ":${port}"
http:
tls:
certresolver: myresolver

certificatesResolvers:
myresolver:
acme:
dnsChallenge:
provider: alidns
delayBeforeCheck: 0
storage: /root/crt/acme.json
# caserver: "https://acme-staging-v02.api.letsencrypt.org/directory" # test server
accessLog:
filePath: "/root/log/access.log.json"
format: "json"

nexus

Nexus 是一个开源的私有镜像仓库,部署后需要新建一个 hosted 类型的 Docker 仓库。

8081 端口是 Web,8082 端口是 Docker 仓库 端口。

~/docker-compose/nexus/docker-compose.yml

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
version: "3.9"
services:
nexus:
image: sonatype/nexus3
container_name: nexus
restart: unless-stopped
ports:
- "18081:8081"
- "18082:8082"
volumes:
- ~/docker_volume/nexus:/nexus-data # sudo chmod 777 -R ~/docker_volume/nexus
networks:
- traefik
labels:
- "traefik.enable=true"
- "traefik.http.routers.nexus.rule=Host(`nexus.${host}`)"
- "traefik.http.routers.nexus.tls=true"
- "traefik.http.routers.nexus.tls.certresolver=myresolver"
- "traefik.http.routers.nexus.tls.domains[0].main=*.${host}"

- "traefik.http.routers.dockerhub.service=docker-hub-service@file"
- "traefik.http.routers.dockerhub.rule=Host(`dockerhub.${host}`)"
- "traefik.http.routers.dockerhub.tls=true"
- "traefik.http.routers.dockerhub.tls.certresolver=myresolver"
- "traefik.http.routers.dockerhub.tls.domains[0].main=*.${host}"
networks:
traefik:
external: true

gitlab

~/docker-compose/gitlab/docker-compose.yml

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
version: "3.9"
services:
gitlab:
image: 'gitlab/gitlab-ee'
container_name: gitlab
restart: unless-stopped
hostname: 'gitlab.${host}:${port}'
environment:
GITLAB_OMNIBUS_CONFIG: |
external_url 'https://gitlab.${host}:${port}'
nginx['listen_https'] = false
nginx['listen_port'] = 80
# # Add any other gitlab.rb configuration here, each on its own line
ports:
- '18000:80'
- '10022:22'
volumes:
- '~/docker_volume/gitlab/config:/etc/gitlab'
- '~/docker_volume/gitlab/logs:/var/log/gitlab'
- '~/docker_volume/gitlab/data:/var/opt/gitlab'
labels:
- "traefik.enable=true"
- "traefik.http.routers.gitlab.rule=Host(`gitlab.${host}`)"
- "traefik.http.routers.gitlab.tls=true"
- "traefik.http.routers.gitlab.tls.certresolver=myresolver"
- "traefik.http.routers.gitlab.tls.domains[0].main=*.${host}"
- "traefik.http.services.gitlab-gitlab.loadbalancer.server.port=80"
networks:
- traefik
gitlab-runner:
image: 'gitlab/gitlab-runner'
container_name: gitlab-runner
restart: unless-stopped
volumes:
- '~/docker_volume/gitlab/runner:/etc/gitlab-runner'
- '/var/run/docker.sock:/var/run/docker.sock'
privileged: true
networks:
traefik:
external: true
1
2
3
docker exec -it gitlab bash
gitlab-rake "gitlab:password:reset"
docker run --rm -it -v ~/docker_volume/gitlab/runner:/etc/gitlab-runner gitlab/gitlab-runner register

watchtower

~/docker-compose/watchtower/docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
version: "3.9"
services:
watchtower:
image: containrrr/watchtower
container_name: watchtower
restart: unless-stopped
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ~/.docker/config.json:/config.json
networks:
- traefik
command: --http-api-update --interval 60 --cleanup --debug
environment:
- WATCHTOWER_HTTP_API_TOKEN=12345678 # EDIT !!!
labels:
- "traefik.enable=true"
- "traefik.http.routers.watchtower.rule=Host(`watchtower.${host}`)"
- "traefik.http.routers.watchtower.tls=true"
- "traefik.http.routers.watchtower.tls.certresolver=myresolver"
- "traefik.http.routers.watchtower.tls.domains[0].main=*.${host}"
networks:
traefik:
external: true

Project CI

https://github.com/117503445/goframe_learn 为例

./gitlab-ci.yml

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
stages:
- build
- trigger

build-docker-image:
image: docker:dind
stage: build
before_script:
- docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY
# Default branch leaves tag empty (= latest tag)
# All other branches are tagged with the escaped branch name (commit ref slug)
script:
- |
if [[ "$CI_COMMIT_BRANCH" == "$CI_DEFAULT_BRANCH" ]]; then
tag=""
echo "Running on default branch '$CI_DEFAULT_BRANCH': tag = 'latest'"
else
tag=":$CI_COMMIT_REF_SLUG"
echo "Running on branch '$CI_COMMIT_BRANCH': tag = $tag"
fi
- docker build -t "$CI_REGISTRY_IMAGE${tag}" -f Dockerfile_dev .
- docker push "$CI_REGISTRY_IMAGE${tag}"
# Run this job in a branch where a Dockerfile exists
rules:
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile_dev

watchtower-trigger:
image: alpine:3.13
stage: trigger
before_script:
- "sed -i 's/dl-cdn.alpinelinux.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apk/repositories"
- "apk --no-cache add curl"
script:
- 'curl -H "Authorization: Bearer $CI_ZJ_WATCHTOWER_TOKEN" $CI_ZJ_WATCHTOWER_API'
rules:
- if: $CI_COMMIT_BRANCH
exists:
- Dockerfile_dev