介绍
之前入手了一块树莓派 4B, 但一直放在角落里吃灰。最近看到 GitHub Copilot 开始收费,感觉自己就像在给巨硬打白工,于是心血来潮想要在树莓派上搭建一个 Gitea 和 Drone 服务。
这篇博客用来记录我的一些操作。由于截止我写博客的时候已经基本部署完成了,所以就想到什么写什么,中间可能有遗漏的地方。
安装和初始配置
为了节省资源,这次我安装的是 Raspberry Pi OS Lite (64-bit).
安装完成之后启动,执行 sudo raspi-config
配置网络和一些其它的系统选项。
接下来换国内源,执行 sudo apt update && sudo apt upgrade
更新一下。
为了省事,我写了一个自动更新脚本并用 crontab
定期执行更新任务。
/home/sainnhe/repo/dotfiles/scripts/crontab/debian-upgrade.sh
:
1
2
3
4
5
|
#!/usr/bin/env sh
apt update
apt upgrade
apt autoremove
|
赋予可执行权限: chmod +x /home/sainnhe/repo/dotfiles/scripts/crontab/debian-upgrade.sh
编辑 crontab: sudo crontab -e
添加下面这一行:
1
|
0 13 * * 0 /home/sainnhe/repo/dotfiles/scripts/crontab/debian-upgrade.sh
|
这是每周星期天 13:00 执行更新脚本。
然后启用系统服务:
1
2
|
$ sudo systemctl enable --now cron.service
$ sudo systemctl restart cron.service
|
配置 SSH
安装并启用 SSH Server:
1
2
|
$ sudo apt install openssh-server
$ sudo systemctl enable --now ssh.service
|
在 PC 上生成 SSH 密钥:
1
2
3
4
|
$ ssh-keygen -t ecdsa -f ~/.ssh/id_ecdsa -C "[email protected]"
$ eval "$(ssh-agent -s)"
$ ssh-add ~/.ssh/id_ecdsa
$ kill <ssh-agent-pid> # <ssh-agent-pid> 就是上上步打印的 PID
|
然后把 PC 上的 ~/.ssh/id_ecdsa.pub
里面的内容添加到树莓派的 ~/.ssh/authorized_keys
。
在树莓派上查看一下 IP 地址 ip addr
然后就可以 SSH 连接了。
1
|
$ ssh <user-name>@<ip-addr>
|
为了安全起见,禁用密码登录:
/etc/ssh/sshd_config
:
1
|
PasswordAuthentication no
|
重启一下服务:
1
|
$ sudo systemctl restart ssh.service
|
我配置了自动连接 Wifi, 不过 IP 地址是通过 DHCP 动态分配的,下次启动可能就变了,所以我想要找到一种解决方案,不需要用 ip addr
查看 IP 就能 SSH 上去。
方案一:使用 DDNS
DDNS 即动态 DNS, 可以实时更新一条 DNS 记录。
我买了一个域名 sainnhe.dev
,托管在 Google Domains,然后我在 DNS 管理的页面添加一条 DDNS 记录 ssh.local.sainnhe.dev
,添加完之后可以查看这条记录的用户名和密码。
接下来安装并启用 ddclient
,这是一个 DDNS 客户端。
1
2
|
$ sudo apt install ddclient
$ sudo systemctl enable --now ddclient
|
编辑配置 /etc/ddclient.conf
:
1
2
3
4
5
6
7
8
|
protocol=dyndns2
use=if
if=wlan0
server=google-domains.vercel.app
ssl=yes
login=<username>
password=<password>
ssh.local.sainnhe.dev
|
其中 <username>
和 <password>
分别是这条 DDNS 记录的用户名和密码。
这个配置的意思是,用 wlan0
这个 Interface 返回的 IP 地址作为 DDNS 记录的值,这其实就是 Wifi 连接的内网 IP 地址。
接下来用 <username>
和 <password>
作为登录凭证,将托管在 Google Domains 的 ssh.local.sainnhe.dev
这个域名的记录更新为内网 IP 地址。
由于原版的域名 domains.google.com
在国内无法直接访问,所以这里用的是我用 Vercel 部署的一个反代 google-domains.vercel.app
。
重启一下服务:
1
|
$ sudo systemctl restart ddclient.service
|
测试一下看看有没有更新成功:
1
|
$ dig @8.8.8.8 ssh.local.sainnhe.dev +short
|
如果返回的是内网 IP 地址,说明更新成功了。
接下来每次启动树莓派,它都会先自动连接 Wifi, 然后用自动将 ssh.local.sainnhe.dev
这条记录的值设置成内网 IP 地址。
现在就可以拔掉 HDMI 线和键盘线,把树莓派放到柜子里(隔音,风扇声还挺吵的),拉一条 Type-C 的电源线接上去,然后就可以用新的域名登录了:
方案二:使用脚本更新
如果域名托管在了不支持 DDNS 的服务商(比如 Cloudflare),那么可以考虑用脚本来自动更新 DNS 记录。
如果你家的网络有公网 IP, 那么可以考虑用 timothymiller/cloudflare-ddns。
如果你和我一样只有局域网 IP, 那么可以用我的 Fork。
这个 Fork 会把 wlan0 的 IPv4 地址更新到目标域名,详细使用方法参考 README 。
Nftables
接下来配置防火墙。这一步其实是可选的,因为我们最后会用内网穿透来把 Web 服务暴露到公网当中,而这并不需要开启某个端口。
但如果你想要在内网访问的话,就需要在防火墙中开启对应的 Web 服务的端口了。
这里我们以 Gitea 的 3000
端口为例,先禁用 iptables
并启用 nftables
:
1
2
3
|
$ sudo systemctl disable --now iptables.service
$ sudo apt install nftables
$ sudo systemctl enable --now nftables.service
|
/etc/nftables.conf
:
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
101
102
103
|
#!/sbin/nft -f
#
# nftables.conf: nftables config for server firewall
#
# input chain
# -----------
# * accept all traffic related to established connections
# * accept all traffic on loopback iface and wireguard iface
# * accept icmp, https, and wireguard traffic on external iface
# * drop and count any other input traffic
#
# forward chain
# -------------
# * accept all forwarded traffic (TODO: lock this down more)
#
# output chain
# ------------
# * accept and count all output traffic (TODO: lock this down more)
#
# Commands
# --------
# * `nft list counters`: to show counter values
# * `nft list ruleset`: list rules
# * `nft monitor`: monitor traces
# * `nft monitor trace | grep 'output packet'`: monitor out traffic
# * `nft -f /etc/nftables-reset.conf`: disable filters
#
# Notes
# -----
# * See commented "log" line below to log dropped input headers
# * Used to need to enable non-wg http for certbot, but that isn't
# necessary now because of `certbot-dns-linode`
#
# clear rules
flush ruleset
table inet filter {
# declare named counters
counter drop_ct_invalid {}
counter accept_ct_rel {}
counter drop_loop_v4 {}
counter drop_loop_v6 {}
counter accept_icmp_v4 {}
counter accept_icmp_v6 {}
counter accept_ssh {}
counter accept_gitea_http {}
counter accept_wg {}
counter drop_input {}
counter accept_output {}
chain input {
# input policy: drop
type filter hook input priority 0; policy drop;
# connection tracker
ct state invalid counter name drop_ct_invalid drop \
comment "drop ct invalid"
ct state {established, related} counter name accept_ct_rel accept \
comment "accept ct established, related"
# accept all loopback traffic
iif lo accept comment "accept iif lo"
# drop loopback traffic on non-loopback interfaces
iif != lo ip daddr 127.0.0.1/8 counter name drop_loop_v4 drop \
comment "drop invalid loopback traffic"
iif != lo ip6 daddr ::1/128 counter name drop_loop_v6 drop \
comment "drop invalid loopback traffic"
# accept icmp
ip protocol icmp counter name accept_icmp_v4 accept \
comment "accept icmp v4"
ip6 nexthdr icmpv6 counter name accept_icmp_v6 accept \
comment "accept icmp v6"
# accept external ssh
tcp dport 22 counter name accept_ssh accept comment "accept ssh"
# accept external http
tcp dport 3000 counter name accept_gitea_http accept comment "accept gitea http"
# count/log remaining (disabled because of log spam)
# counter name drop_input log prefix "DROP " comment "drop input"
# count remaining (no logging)
counter name drop_input comment "drop input"
}
# accept all forwarding (TODO: lock this down more)
chain forward {
# forward policy: accept
type filter hook forward priority 0; policy accept;
}
# count/accept all output (TODO: lock this down more)
chain output {
# output policy: accept
type filter hook output priority 0; policy accept;
counter name accept_output comment "accept output"
}
}
|
这里我们启用了 22
和 3000
这两个端口,其它端口全部禁用。
然后重启一下 Nftables:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
$ sudo systemctl restart nftables.service
$ sudo systemctl status nftables.service
● nftables.service - nftables
Loaded: loaded (/lib/systemd/system/nftables.service; enabled; vendor preset: enabled)
Active: active (exited) since Thu 2022-08-04 09:30:07 CST; 5 days ago
Docs: man:nft(8)
http://wiki.nftables.org
Main PID: 69970 (code=exited, status=0/SUCCESS)
Tasks: 0 (limit: 8775)
CPU: 0
CGroup: /system.slice/nftables.service
Aug 04 09:30:07 raspberrypi systemd[1]: Starting nftables...
Aug 04 09:30:07 raspberrypi systemd[1]: Finished nftables.
|
Fail2ban
Fail2ban 可以用来防止 DDoS 攻击,如果你希望通过内网访问你的 Web 服务的话,可以配置一下,否则如果你只是想要在公网中访问,那就没必要配置,因为我们接下来会用 Cloudflare Tunnel 来进行内网穿透,而 CF 家的服务本身就自带防 DDoS 攻击的功能。
同样以 Gitea 的 3000
端口为例,先安装并启用 Fail2ban:
1
2
|
$ sudo apt install fail2ban
$ sudo systemctl enable --now fail2ban.service
|
写一下配置。
/etc/fail2ban/filter.d/gitea.conf
:
1
2
3
4
|
# gitea.conf
[Definition]
failregex = .*(Failed authentication attempt|invalid credentials|Attempted access of unknown user).* from <HOST>
ignoreregex =
|
/etc/fail2ban/jail.d/gitea.conf
:
1
2
3
4
5
6
7
8
|
[gitea]
enabled = true
filter = gitea
logpath = /var/lib/gitea/log/gitea.log
maxretry = 10
findtime = 3600
bantime = 900
action = nftables-allports
|
/etc/fail2ban/jail.d/gitea-docker.conf
:
1
2
3
4
5
6
7
8
|
[gitea-docker]
enabled = true
filter = gitea
logpath = /var/lib/gitea/log/gitea.log
maxretry = 10
findtime = 3600
bantime = 900
action = nftables-allports[chain="FORWARD"]
|
重启一下服务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
$ sudo systemctl restart fail2ban.service
$ sudo systemctl status fail2ban.service
● fail2ban.service - Fail2Ban Service
Loaded: loaded (/lib/systemd/system/fail2ban.service; enabled; vendor preset: enabled)
Active: active (running) since Thu 2022-07-28 09:44:40 CST; 1 weeks 5 days ago
Docs: man:fail2ban(1)
Main PID: 524 (fail2ban-server)
Tasks: 9 (limit: 8775)
CPU: 23min 41.582s
CGroup: /system.slice/fail2ban.service
└─524 /usr/bin/python3 /usr/bin/fail2ban-server -xf start
Jul 28 09:44:40 raspberrypi systemd[1]: Starting Fail2Ban Service...
Jul 28 09:44:40 raspberrypi systemd[1]: Started Fail2Ban Service.
Jul 28 09:44:42 raspberrypi fail2ban-server[524]: Server ready
|
Gitea
为了方便更新和维护,这里直接上 Docker:
1
2
|
$ sudo apt install docker docker-compose
$ sudo systemctl enable --now docker.service
|
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"
networks:
gitea:
external: false
services:
server:
image: gitea/gitea:latest
container_name: gitea
environment:
- USER_UID=1000
- USER_GID=1000
restart: always
networks:
- gitea
volumes:
- /srv/gitea:/data
- /etc/timezone:/etc/timezone:ro
- /etc/localtime:/etc/localtime:ro
ports:
- "3000:3000"
- "10000:22"
|
cd
到含有 docker-compose.yml
的目录下,执行 sudo docker-compose up -d
.
这里 3000
是 Gitea 的网页端口,10000
是 SSH 端口,不过我们并不会用到 10000
这个端口,因为我们之后会用 Token 来访问私有仓库,所以就不在 Nftables 里面启用它了。
Docker 可以很方便地自动更新,我写了一个脚本来定时 Pull 最新的 Image:
pull.sh
:
1
2
3
4
5
6
7
8
|
#!/usr/bin/env bash
cd /home/sainnhe/repo/gitea
docker-compose down
docker-compose pull
systemctl daemon-reload
systemctl restart docker
docker-compose up -d
|
1
2
|
$ chmod a+x pull.sh
$ sudo crontab -e
|
1
|
0 0 * * 0 /home/sainnhe/repo/gitea/pull.sh
|
内网穿透
我们现在要将 3000
这个端口暴露到公网当中,这里就要用到内网穿透了。
这里我选了 Cloudflare Tunnel 来实现,免费稳定速度也不错。
首先准备一枚域名,我的是 sainnhe.dev
,并把这个域名托管到 Cloudflare 。
然后安装 cloudflared
:
1
2
3
|
$ wget https://github.sainnhe.dev/cloudflare/cloudflared/releases/download/2022.7.1/cloudflared-linux-arm64.deb
$ sudo dpkg -i cloudflared-linux-arm64.deb
$ rm cloudflared-linux-arm64.deb
|
切换到 Root 账户,登录 Cloudflare:
1
2
|
$ sudo su
# cloudflared tunnel login
|
这时会弹出来一个URL,用浏览器打开,登录认证,然后选择你想用来做内网穿透的域名即可。
成功后会生成证书,放置于 /root/.cloudflared/cert.pem
中。
新建一个隧道:
1
|
# cloudflared tunnel create <tunnel-name>
|
<tunnel-name>
是隧道的名字,可以随意起。
成功后会生成证书,放置于 /root/.cloudflared/<tunnel-uuid>.json
查看一下是否创建成功:
1
|
# cloudflared tunnel list
|
这个命令也会列出隧道的名字和 UUID, 忘了的话可以在这里看。
接下来新建隧道对应的 DNS 记录:
1
|
# cloudflared tunnel route dns <tunnel-name> <subdomain>
|
其中 <subdomain>
是完整的域名(不包含 https),比如我的是 git.sainnhe.dev
现在写配置文件。
/root/.cloudflared/config.yml
:
1
2
3
4
5
6
7
8
|
tunnel: <tunnel-uuid>
credentials-file: /root/.cloudflared/<tunnel-uuid>.json
protocol: http2
no-autoupdate: true
ingress:
- hostname: git.sainnhe.dev
service: http://localhost:3000
- service: http_status:404
|
然后安装并启用系统服务:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
# cloudflared service install
# systemctl start cloudflared
# systemctl status cloudflared
Loaded: loaded (/etc/systemd/system/cloudflared.service; enabled; vendor preset: enabled)
Active: active (running) since Wed 2022-08-10 08:03:43 CST; 15min ago
Main PID: 151623 (cloudflared)
Tasks: 10 (limit: 8775)
CPU: 3.245s
CGroup: /system.slice/cloudflared.service
└─151623 /usr/bin/cloudflared --no-autoupdate --config /etc/cloudflared/config.yml tunnel run
Aug 10 08:03:40 raspberrypi cloudflared[151623]: 2022-08-10T00:03:40Z INF Settings: map[config:/etc/cloudflared/config.yml cred-file:/root/.cloudflared/b4a55bfd-8c33-4ac9-904f-c>
Aug 10 08:03:40 raspberrypi cloudflared[151623]: 2022-08-10T00:03:40Z INF cloudflared will not automatically update if installed by a package manager.
Aug 10 08:03:40 raspberrypi cloudflared[151623]: 2022-08-10T00:03:40Z INF Generated Connector ID: 6618239f-dfec-45fc-988c-02316a0e2da2
Aug 10 08:03:40 raspberrypi cloudflared[151623]: 2022-08-10T00:03:40Z INF Initial protocol http2
Aug 10 08:03:40 raspberrypi cloudflared[151623]: 2022-08-10T00:03:40Z INF Starting metrics server on 127.0.0.1:34093/metrics
Aug 10 08:03:43 raspberrypi cloudflared[151623]: 2022-08-10T00:03:43Z INF Connection 337ef3a3-0b98-40b3-9912-c42c7a7eb108 registered connIndex=0 ip=198.41.200.33 location=SJC
Aug 10 08:03:43 raspberrypi systemd[1]: Started cloudflared.
Aug 10 08:03:44 raspberrypi cloudflared[151623]: 2022-08-10T00:03:44Z INF Connection 8a27ed8e-08e6-4484-80ee-d32f6f2cd66e registered connIndex=1 ip=198.41.192.47 location=LAX
Aug 10 08:03:45 raspberrypi cloudflared[151623]: 2022-08-10T00:03:45Z INF Connection ac04131f-f4fd-44d8-a426-9b214d0e3851 registered connIndex=2 ip=198.41.200.63 location=SJC
Aug 10 08:03:47 raspberrypi cloudflared[151623]: 2022-08-10T00:03:47Z INF Connection 8fd4fe2c-06d0-4f78-a64c-6e87d8e5d01f registered connIndex=3 ip=198.41.192.107 location=LAX
|
你的配置文件会被复制到 /etc/cloudflared/config.yml
, 所以之后的修改就应该在这个文件中进行,而不是原来的 /root/.cloudflared/config.yml
。
部署完成后就可以在 git.sainnhe.dev 中访问 Gitea 了。
由于我们禁用了 SSH 端口,所以无法通过 SSH 克隆。
我是通过 Token 来克隆的。在 Gitea 的设置里生成一个 Token, 然后就可以通过 https://<token>@git.sainnhe.dev/<user>/<repo>.git
来克隆了。
Drone
Gitea 默认不集成 CI/CD,我们再部署一个 Drone, 这是一个开源的 CI/CD 平台。
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'
services:
drone_server:
image: drone/drone:2
container_name: drone-server
restart: always
volumes:
- /srv/drone:/data
env_file:
- server.env
ports:
- 3001:80
drone_runner:
image: drone/drone-runner-docker:1
container_name: drone-runner
restart: always
volumes:
- /var/run/docker.sock:/var/run/docker.sock
env_file:
- runner.env
ports:
- 3002:3000
|
server.env
:
1
2
3
4
5
6
|
DRONE_GITEA_SERVER=https://git.sainnhe.dev
DRONE_GITEA_CLIENT_ID=<your-client-id>
DRONE_GITEA_CLIENT_SECRET=<your-client-secret>
DRONE_RPC_SECRET=<rpc-secret>
DRONE_SERVER_HOST=drone.sainnhe.dev
DRONE_SERVER_PROTO=https
|
各个环境变量的含义及获取方式参考 https://docs.drone.io/server/provider/gitea/
runner.env
:
1
2
3
4
5
6
7
|
DRONE_RPC_HOST=drone.sainnhe.dev
DRONE_RPC_PROTO=https
DRONE_RPC_SECRET=<rpc-secret>
DRONE_RUNNER_CAPACITY=4
DRONE_RUNNER_NAME=main
DRONE_UI_USERNAME=<username>
DRONE_UI_PASSWORD=<password>
|
各个环境变量的含义及获取方式参考 https://docs.drone.io/runner/docker/installation/linux/
执行 sudo docker-compose up -d
开始运行服务。
如果你想在内网中访问它的话,可以再配置一下 Nftables 和 Fail2ban 。
接下来进行内网穿透,先添加两条新的 DNS 记录:
1
2
|
$ sudo cloudflared tunnel route dns <tunnel-name> drone.sainnhe.dev
$ sudo cloudflared tunnel route dns <tunnel-name> drone-runner.sainnhe.dev
|
然后编辑 /etc/cloudflared/config.yml
:
1
2
3
4
5
6
7
8
9
10
11
12
|
tunnel: <tunnel-uuid>
credentials-file: /root/.cloudflared/<tunnel-uuid>.json
protocol: http2
no-autoupdate: true
ingress:
- hostname: git.sainnhe.dev
service: http://localhost:3000
- hostname: drone.sainnhe.dev
service: http://localhost:3001
- hostname: drone-runner.sainnhe.dev
service: http://localhost:3002
- service: http_status:404
|
重启服务:
1
|
$ sudo systemctl restart cloudflared.service
|
过一会就可以访问了。
使用的时候需要注意几点:
- 你想跑 CI/CD 的仓库需要先在 Drone 里启用,如果你看不到你想跑的仓库,点一下主界面右上角的 “Sync” 。
type
必须是 docker
,因为我们是用 docker-compose 安装的。
arch
必须是 arm64
,因为树莓派 4B 的 CPU 是 arm64 结构的。
示例 .drone.yml
:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
kind: pipeline
type: docker
name: default
platform:
os: linux
arch: arm64
steps:
- name: greeting
image: alpine
commands:
- echo hello
- echo world
|
把 .drone.yml
放到项目的根目录下,提交并推送到远程仓库,Drone 就会自动触发流水线了。
补充:
部署过程中如果碰到类似以 “iptables: No chain/target/match by that name.” 结尾的报错,执行以下两条命令解决:
1
2
|
$ sudo systemctl daemon-reload
$ sudo systemctl restart docker
|