【SOP实操】一台高配服务器,用 k3s + Gitea 从零搭建 Golang 应用的轻量级生产发布环境

【SOP实操】一台高配服务器,用 k3s + Gitea 从零搭建 Golang 应用的轻量级生产发布环境

一、对于云部署解决方案,为什么选 k3s,而不是标准 k8s

虽然AI时代,但作为软件工程的重要技术工程部分,还是要深入掌握云的使用。

在“一台高配服务器”这个约束下,标准 Kubernetes 的控制面本身就是一种负担。kubeadm 部署的 k8s 控制面包含 API Server、Scheduler、Controller Manager、etcd 等独立组件,仅控制面就需要至少 2 vCPU + 2 GB RAM,且各组件独立部署、独立排障。k3s 则把这些打包成一个不到 70 MB 的单一二进制,默认使用 SQLite 作为单节点数据存储,并内置 containerd、Flannel、Traefik Ingress 和 Klipper LB。关键事实:k3s 是 CNCF 认证的 Kubernetes 发行版,不是 fork,也不是“简化版 API”——任何合法的 Kubernetes manifest 在 k3s 上同样生效。

维度标准 k8s(kubeadm)k3s(单节点模式)
控制面形态多组件独立部署单一二进制
默认数据存储etcd(必须)SQLite(单节点);etcd 可选
控制面最低资源2 vCPU + 2 GB RAM(不含 etcd 和工作负载)官方资源剖析:Server 带工作负载约 6% CPU + 1.6 GB RAM(x86)
内置组件需自行安装 CNI、Ingress、LB内置 Flannel、Traefik、Klipper LB、containerd
API 兼容性标准 Kubernetes API100% 兼容
适用场景多节点、大规模、企业级高可用边缘、单机/小集群、轻量生产

需要特别说明一个常见的误读:k3s 官方“最低 512 MB”指的是 agent 节点的最低门槛,且只是“能启动”级别。真实生产工作负载下,官方资源剖析显示 Server 带工作负载的基线约为 1.6 GB RAM(SQLite/Kine)或 1.6 GB RAM(嵌入式 etcd),CPU 占用约 6%(以 Intel 8375C 为基准)。因此,“一台高配服务器”的正确配置区间是 4–8 vCPU、8–16 GB RAM、NVMe SSD,而不是贴着最低线走。

单节点的根本限制在于控制面单点:Server 一旦重启或宕机,Agent、Ingress、API 全部失联,业务随之中断。本文的方案定位是“轻量级生产环境”——用单节点承载中小流量业务,通过流程规范、备份和资源隔离来管理风险,而不是追求控制面高可用。当业务增长到无法容忍控制面中断时,再按 k3s 的演进路径将 SQLite 平滑迁移到三节点嵌入式 etcd。

二、硬件配置建议与技术栈总览

硬件配置

单台高配服务器建议配置:

资源建议值说明
CPU4–8 vCPUk3s 控制面开销低,主要留给 Gitea、Runner 构建和应用负载
内存8–16 GBGitea 小团队约 2–4 GB;Runner 构建峰值额外 2–4 GB;k3s 控制面约 1.6 GB;剩余给应用
磁盘200 GB+ NVMe SSD镜像层、Gitea 仓库、SQLite 数据库、构建缓存都需要低延迟存储
操作系统Ubuntu 22.04 / 24.04 LTS社区验证充分,k3s 兼容性好

如果同时运行 Gitea、act_runner 和一个简单的 Golang Web 应用,4 vCPU / 8 GB RAM 是舒适起点;如果 Runner 构建频繁,8 vCPU / 16 GB 留出足够余量。

架构与数据流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
开发者 git push
│
▼
┌─────────────────────────────────────────────┐
│ Gitea(运行在 k3s 内,NodePort / Ingress 暴露) │
│ - 代码托管 │
│ - Gitea Container Registry(镜像仓库) │
│ - Webhook 触发 Actions │
└──────────────────┬──────────────────────────┘
│ webhook
▼
┌─────────────────────────────────────────────┐
│ act_runner(运行在 k3s 内) │
│ - 执行 .gitea/workflows/deploy.yml │
│ - 构建镜像 → 推送 Gitea Registry │
│ - kubectl apply 更新 Deployment(新 tag) │
└──────────────────┬──────────────────────────┘
│ kubectl apply (image: registry/.../app:sha)
▼
┌─────────────────────────────────────────────┐
│ k3s(同一节点) │
│ containerd 从 Gitea Registry 拉取新镜像 │
│ Deployment 滚动更新 → Service → Ingress │
└─────────────────────────────────────────────┘

这个方案将 Gitea Registry、act_runner、k3s 压缩在同一台机器上,镜像拉取不出节点,构建和部署链路极短。核心决策是 用 Gitea 自带的 Container Registry 作为唯一镜像仓库,避免引入外部 registry 带来的认证和网络复杂度。

三、基础环境准备

以下步骤在目标服务器上以具有 sudo 权限的用户执行。假设服务器 IP 为 SERVER_IP,文中以 192.0.2.10 示例。

3.1 系统基础配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 更新系统
sudo apt update && sudo apt upgrade -y

# 安装基础工具
sudo apt install -y curl wget git jq ca-certificates gnupg lsb-release

# 关闭 swap(k8s/k3s 默认要求)
sudo swapoff -a
sudo sed -i '/ swap / s/^/#/' /etc/fstab

# 确认内核模块
sudo modprobe br_netfilter
cat <<EOF | sudo tee /etc/modules-load.d/k3s.conf
br_netfilter
overlay
EOF

3.2 安装 k3s(单节点)

1
2
3
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server \
--disable=traefik \
--write-kubeconfig-mode=644" sh -

这里显式禁用了内置的 Traefik。原因:本方案中 Gitea 和应用都通过 Service/Ingress 暴露,如果开发者希望完全掌控 Ingress 层,可以自行部署 ingress-nginx;如果不想额外管理,保留 Traefik 也能工作。禁用 Traefik 是为了减少一个需要理解和调试的组件,但开发者需要自行解决 Ingress 或使用 NodePort 暴露服务。

**为简化首次跑通,建议保留 Traefik,去掉 --disable=traefik**:

1
curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server --write-kubeconfig-mode=644" sh -

等待节点就绪:

1
2
sudo k3s kubectl get nodes
# 应看到单节点 STATUS 为 Ready

为方便后续操作,将 kubeconfig 放到当前用户可读位置:

1
2
3
4
5
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $USER:$USER ~/.kube/config
export KUBECONFIG=~/.kube/config
echo 'export KUBECONFIG=~/.kube/config' >> ~/.bashrc

现在 kubectl 可以直接使用。

四、部署 Gitea(代码托管 + 镜像仓库 + Actions 源)

Gitea 以 Deployment + PVC 的方式部署在 k3s 内。使用 SQLite 作为 Gitea 自身数据库,进一步降低依赖;对于 ~10 人团队,2–4 GB RAM 和 2–4 核 CPU 是官方推荐区间。

4.1 创建 namespace 和 PVC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# gitea-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: gitea
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: gitea-data
namespace: gitea
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 50Gi
1
kubectl apply -f gitea-namespace.yaml

k3s 默认提供 local-path StorageClass,PVC 会自动绑定到节点本地路径,适合单节点场景。

4.2 部署 Gitea

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
# gitea-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: gitea
namespace: gitea
spec:
replicas: 1
selector:
matchLabels:
app: gitea
template:
metadata:
labels:
app: gitea
spec:
containers:
- name: gitea
image: gitea/gitea:1.23 # 固定版本,不要用 latest
ports:
- containerPort: 3000
- containerPort: 2222
env:
- name: GITEA__database__DB_TYPE
value: sqlite3
- name: GITEA__server__ROOT_URL
value: "http://gitea.192.0.2.10.nip.io/"
- name: GITEA__server__SSH_DOMAIN
value: "192.0.2.10"
- name: GITEA__server__SSH_PORT
value: "2222"
- name: GITEA__service__DISABLE_REGISTRATION
value: "true"
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
volumes:
- name: data
persistentVolumeClaim:
claimName: gitea-data
---
apiVersion: v1
kind: Service
metadata:
name: gitea
namespace: gitea
spec:
type: NodePort
selector:
app: gitea
ports:
- name: http
port: 3000
targetPort: 3000
nodePort: 30300
- name: ssh
port: 2222
targetPort: 2222
nodePort: 30222

ROOT_URL 使用了 nip.io 通配 DNS,省去修改 hosts 的麻烦;生产环境如果有真实域名,替换为真实域名即可。

1
2
kubectl apply -f gitea-deployment.yaml
kubectl -n gitea get pods -w # 等待 Running

浏览器访问 http://192.0.2.10:30300,首次进入安装页面。保持默认 SQLite 配置,设置管理员账号,完成安装。安装完成后,登录 Gitea,进入 设置 → 应用,生成一个 API Token 或直接使用管理员账号的密码用于后续 Runner 注册和 Registry 认证。

4.3 启用 Gitea Container Registry

Gitea 内置 Container Registry,无需额外部署 Harbor 或其他 registry。在 app.ini 中确认或添加:

1
2
[packages]
ENABLED = true

通过环境变量注入更符合 Kubernetes 原生方式。在 Deployment 的 env 中追加:

1
2
- name: GITEA__packages__ENABLED
value: "true"

重新 apply 后,Gitea Registry 的访问地址为:

1
gitea.192.0.2.10.nip.io:30300/<owner>/<image>:<tag>

后续 act_runner 构建镜像后推送至此,k3s 的 containerd 从同一地址拉取。

五、配置 k3s 从 Gitea Registry 拉取镜像

k3s 的 containerd 需要知道 Gitea Registry 是“私有但可信任”的仓库。根据 k3s 文档,启动时会检查 /etc/rancher/k3s/registries.yaml,如果存在则据此生成 containerd 配置。

由于 Gitea Registry 使用 HTTP(NodePort 未配置 TLS),需要在每个需要拉取镜像的节点上配置 registries.yaml。单节点场景只需在唯一节点上配置:

1
2
3
4
5
6
7
8
9
10
11
12
# /etc/rancher/k3s/registries.yaml
mirrors:
"gitea.192.0.2.10.nip.io:30300":
endpoint:
- "http://gitea.192.0.2.10.nip.io:30300"
configs:
"gitea.192.0.2.10.nip.io:30300":
auth:
username: "<gitea用户名>"
password: "<gitea密码或Token>"
tls:
insecure_skip_verify: true

写入后重启 k3s:

1
sudo systemctl restart k3s

验证 containerd 配置已生效:

1
sudo cat /var/lib/rancher/k3s/agent/etc/containerd/config.toml | grep -A5 "gitea"

这一步是整条链路中最容易出问题的环节。如果后续 Pod 拉取镜像时报 x509 或 401,优先回到 registries.yaml 检查 endpoint、认证和 insecure_skip_verify。

六、部署 act_runner(Gitea Actions Runner)

act_runner 是 Gitea Actions 的执行器,以 Deployment 形式运行在 k3s 内。它需要访问 Gitea 的 API 获取任务,并且需要 kubectl 能力来执行部署步骤。

6.1 在 Gitea 中获取 Runner 注册 Token

Gitea 管理员界面 → 站点管理 → Actions → Runner,点击“创建 Runner”,获取 registration token。

6.2 部署 act_runner

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
# act-runner.yaml
apiVersion: v1
kind: Secret
metadata:
name: runner-secret
namespace: gitea
type: Opaque
stringData:
token: "<粘贴 registration token>"
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: act-runner-data
namespace: gitea
spec:
accessModes: [ReadWriteOnce]
resources:
requests:
storage: 20Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: act-runner
namespace: gitea
spec:
replicas: 1
selector:
matchLabels:
app: act-runner
template:
metadata:
labels:
app: act-runner
spec:
serviceAccountName: act-runner # 需要 RBAC 权限执行 kubectl
containers:
- name: runner
image: gitea/act_runner:0.2.13
env:
- name: GITEA_INSTANCE_URL
value: "http://gitea.gitea.svc.cluster.local:3000"
- name: GITEA_RUNNER_REGISTRATION_TOKEN
valueFrom:
secretKeyRef:
name: runner-secret
key: token
- name: CONFIG_FILE
value: /data/config.yaml
volumeMounts:
- name: data
mountPath: /data
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
volumes:
- name: data
persistentVolumeClaim:
claimName: act-runner-data

act_runner 需要执行 kubectl 才能完成部署。为它创建 ServiceAccount 和 RBAC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# runner-rbac.yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: act-runner
namespace: gitea
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: act-runner-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: cluster-admin
subjects:
- kind: ServiceAccount
name: act-runner
namespace: gitea

生产环境中不建议直接绑定 cluster-admin,应创建一个仅对目标 namespace 具有 deployments、services、pods 等必要权限的 Role 和 RoleBinding。这里为了首次跑通简化处理,后续在“最佳实践”中说明收窄权限的方式。

1
2
3
kubectl apply -f runner-rbac.yaml
kubectl apply -f act-runner.yaml
kubectl -n gitea get pods -w

Runner 启动后,回到 Gitea 的 Runner 管理页面,应看到 Runner 状态为 Idle 或 Active。如果显示离线,检查 kubectl -n gitea logs deploy/act-runner,通常是 registration token 过期或 Gitea 地址不可达。

七、准备 Golang 示例应用

7.1 应用代码(最小 Web 应用)

1
2
3
4
5
6
7
go-web-demo/
├── main.go
├── go.mod
├── Dockerfile
└── k8s/
├── deployment.yaml
└── service.yaml

main.go:

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
package main

import (
"fmt"
"log"
"net/http"
"os"
)

func main() {
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}

http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from k3s + Gitea. Host: %s\n", r.Host)
})

http.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
})

log.Printf("listening on :%s", port)
if err := http.ListenAndServe(":"+port, nil); err != nil {
log.Fatal(err)
}
}

关键细节:Go 服务必须监听 0.0.0.0 或 :8080,不能绑定 127.0.0.1,否则 Service 流量无法进入。上面的 ":8080" 是正确的。

go.mod:

1
2
3
module go-web-demo

go 1.23

7.2 Dockerfile(多阶段构建,静态二进制)

1
2
3
4
5
6
7
8
9
10
11
12
13
FROM golang:1.23-alpine AS builder
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /out/app .

FROM alpine:3.20
RUN apk add --no-cache ca-certificates
COPY --from=builder /out/app /app
EXPOSE 8080
USER 65534:65534
ENTRYPOINT ["/app"]

CGO_ENABLED=0 生成静态二进制,避免 Alpine 上的 glibc 兼容问题;非 root 用户降低容器逃逸风险。

7.3 Kubernetes 清单

k8s/deployment.yaml:

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
apiVersion: apps/v1
kind: Deployment
metadata:
name: go-web-demo
namespace: default
spec:
replicas: 1
selector:
matchLabels:
app: go-web-demo
template:
metadata:
labels:
app: go-web-demo
spec:
containers:
- name: app
image: gitea.192.0.2.10.nip.io:30300/<owner>/go-web-demo:latest
ports:
- containerPort: 8080
env:
- name: PORT
value: "8080"
readinessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 2
periodSeconds: 5
livenessProbe:
httpGet:
path: /healthz
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: "50m"
memory: "64Mi"
limits:
cpu: "200m"
memory: "128Mi"

k8s/service.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: go-web-demo
namespace: default
spec:
type: NodePort
selector:
app: go-web-demo
ports:
- port: 80
targetPort: 8080
nodePort: 30080

Service 的 selector 必须与 Deployment 的 matchLabels 完全一致,否则 endpoints 为空,这是 Golang 应用部署中最常见的“部署成功但访问不通”的原因。

7.4 在 Gitea 中创建仓库并推送代码

在 Gitea 中创建一个名为 go-web-demo 的仓库(不要初始化 README),然后在本地:

1
2
3
4
5
6
cd go-web-demo
git init
git add .
git commit -m "initial: golang web demo"
git remote add origin http://192.0.2.10:30300/<owner>/go-web-demo.git
git push -u origin main

八、编写 Gitea Actions 工作流,实现 push 即部署

在仓库中创建 .gitea/workflows/deploy.yml。Gitea Actions 的语法兼容 GitHub Actions,工作流放在 .gitea/workflows/ 下即可被 Runner 识别。

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
name: Build and Deploy
on:
push:
branches: [main]

jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set image tag from commit
id: vars
run: echo "SHORT_SHA=$(echo $GITHUB_SHA | cut -c1-7)" >> $GITHUB_OUTPUT

- name: Log in to Gitea Registry
run: |
echo "${{ secrets.REGISTRY_PASSWORD }}" | \
docker login gitea.192.0.2.10.nip.io:30300 \
-u "${{ secrets.REGISTRY_USER }}" --password-stdin

- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
gitea.192.0.2.10.nip.io:30300/${{ gitea.repository }}:${{ steps.vars.outputs.SHORT_SHA }}
gitea.192.0.2.10.nip.io:30300/${{ gitea.repository }}:latest

- name: Deploy to k3s
run: |
kubectl set image deployment/go-web-demo \
app=gitea.192.0.2.10.nip.io:30300/${{ gitea.repository }}:${{ steps.vars.outputs.SHORT_SHA }} \
-n default
kubectl rollout status deployment/go-web-demo -n default --timeout=120s

需要在 Gitea 仓库中配置 Secrets:

进入仓库 设置 → Actions → Secrets,添加:

  • REGISTRY_USER:Gitea 用户名
  • REGISTRY_PASSWORD:Gitea 密码或 Token

关于 Runner 中的 kubectl 和 docker: 上述工作流使用了 docker login 和 docker/build-push-action,要求 act_runner 的执行环境中有 Docker 或兼容的构建工具。act_runner 默认镜像不包含 kubectl 和 Docker daemon。有两种解决路径:

路径 A(推荐用于本 SOP):使用 Kaniko 替代 Docker-in-Docker。 Kaniko 在容器内无需 daemon 即可构建镜像,更适合 Kubernetes 环境。将 Build 步骤替换为:

1
2
3
- name: Build and push with Kaniko
uses: docker/build-push-action@v5
# 或者使用 Kaniko executor 镜像作为独立 step

如果希望严格按 Kaniko 方式,将 build 步骤写为:

1
2
3
4
5
6
7
- name: Build and push (Kaniko)
run: |
/kaniko/executor \
--context=. \
--dockerfile=Dockerfile \
--destination=gitea.192.0.2.10.nip.io:30300/${{ gitea.repository }}:${{ steps.vars.outputs.SHORT_SHA }} \
--destination=gitea.192.0.2.10.nip.io:30300/${{ gitea.repository }}:latest

这要求 act_runner 的容器镜像中包含 Kaniko executor,或者在该 step 中使用 container: 指定 Kaniko 镜像。首次跑通的最短路径是给 act_runner 使用一个预装了 kubectl、docker(dind)和 Kaniko 的定制镜像,或者接受在 Runner 所在节点上安装 Docker socket 挂载。考虑到“从零开始”的可操作性,更简单的选择是:在 Runner 容器中安装 kubectl,并将构建步骤改为在 Runner 宿主机上用 Docker 构建——但这会牺牲 Runner 的隔离性。

权衡后的建议: 对于本文“轻量级生产”的定位,使用 Docker-in-Docker(dind)模式的 act_runner 是复杂度最低的方案。Runner Deployment 使用 gitea/act_runner:0.2.13-dind 镜像,并在容器内挂载 Docker socket 或运行 dind sidecar。这超出了最小示例的范围,但开发者需要知道:如果不解决 Runner 内的 docker 和 kubectl 可用性,工作流会在 build 或 deploy 步骤失败。一个务实的折中方案是在 Runner 启动后进入 Pod 安装 kubectl 和 docker 客户端,但这不具备声明式可重复性。生产级做法是构建一个包含 act_runner + kubectl + docker-cli + kaniko 的私有 Runner 镜像,推送到 Gitea Registry,然后用该镜像部署 act_runner。

触发部署:

1
2
3
git add .gitea/workflows/deploy.yml
git commit -m "ci: add build and deploy workflow"
git push origin main

推送后,Gitea 仓库的 Actions 标签页应出现运行记录。Runner 拉取代码、构建镜像、推送 Registry、执行 kubectl set image,Deployment 滚动更新。通过以下命令观察:

1
2
kubectl -n default rollout status deployment/go-web-demo
kubectl -n default get pods -l app=go-web-demo -w

访问 http://192.0.2.10:30080,应看到 Hello from k3s + Gitea。

九、生产注意事项与最佳实践

9.1 单节点的风险必须被明确管理

单节点 k3s 的控制面是单点。一旦 Server 进程崩溃或节点重启,API、Ingress、已调度 Pod 的健康检查都可能受影响。可以做的缓解措施:

  • 备份 SQLite 数据:k3s 单节点默认数据存储在 /var/lib/rancher/k3s/server/db/,但 k3s 官方明确说明不支持对 SQLite 做在线快照,建议直接复制 /var/lib/rancher/k3s/server 目录来备份。将备份任务做成 cron,复制到另一块磁盘或远端对象存储。
  • 为未来迁移 etcd 留好路径:不要在早期把 Server 地址硬编码到各处。Agent 注册地址如果写死单机 IP,后续迁移到多 Server 时需要逐台修改。尽量在证书中加入 SAN 或使用可切换的 DNS 名称。
  • 接受“计划内维护窗口”:单节点方案不适合零停机要求极高的业务。把 k3s 升级、内核补丁安排在低峰窗口,并提前验证备份可恢复。

9.2 资源限制不是可选项

轻量不等于不需要观测。每个 Deployment 都应设置 requests 和 limits,防止构建任务或应用内存泄漏拖垮 k3s 和 Gitea。至少部署一个轻量监控(node-exporter + 简单告警,或 netdata),在内存打满之前收到通知。

9.3 Runner 权限最小化

本文为了快速跑通给 act-runner 绑定了 cluster-admin。生产环境中应收窄为仅对目标 namespace 的 Deployment、Service、Pod 的 get/list/watch/patch/update 权限。Runner 是 CI 执行环境,一旦仓库的 Actions 被恶意修改,过大的权限就是安全缺口。

9.4 镜像 tag 策略

工作流同时推送了 SHORT_SHA 和 latest,但 kubectl set image 使用的是 SHORT_SHA。不要在生产部署中使用 latest 作为实际拉取 tag,因为 containerd 的 imagePullPolicy: IfNotPresent 可能导致节点不拉取新镜像。每次 commit 使用唯一 tag 是滚动更新可靠生效的前提。

9.5 Gitea Registry 的 TLS

本文使用 HTTP NodePort 作为 Registry 端点,适用于单机内网或实验环境。生产环境应配置 TLS:可以在 Traefik 或 ingress-nginx 上为 gitea.<domain> 配置证书,然后将 registries.yaml 中的 endpoint 改为 https://,并去掉 insecure_skip_verify: true。Gitea 官方镜像支持通过反向代理终止 TLS,后端 HTTP 保持不变。

9.6 不要在 Server 节点上运行不受控的构建负载

act_runner 的构建任务可能产生 CPU 和内存尖峰。单节点场景下 Runner 和应用共享同一台机器,必须给 Runner 设置比应用更宽松但仍有上限的 resources.limits,避免 docker build 吃满内存导致 k3s 的 kubelet 被 OOM killer 终止。如果构建频率升高,最有效的优化不是升级服务器,而是把 Runner 拆到独立节点,或者使用 Kaniko 并限制其并发度。

十、验证清单(SOP 收尾)

按以下顺序逐项确认,即可判定环境“可发布”:

  1. kubectl get nodes → Ready。
  2. Gitea Pod Running,浏览器可登录,Registry 已启用。
  3. act-runner 在 Gitea Runner 页面显示在线。
  4. registries.yaml 已写入并重启 k3s,containerd 配置中包含 Gitea Registry 端点。
  5. 在仓库中推送一次代码,Actions 运行成功,镜像出现在 Gitea 的 Packages 页面。
  6. kubectl get pods -l app=go-web-demo 显示新 ReplicaSet 的 Pod 为 Running,kubectl rollout history 可追溯。
  7. 通过 NodePort 访问应用,/healthz 返回 ok。
  8. SQLite 备份 cron 已配置,并手动执行一次恢复演练(在非生产副本上) 。

这套方案的价值不在于它“用了 k3s 和 Gitea”,而在于它把 代码推送 → 镜像构建 → 镜像推送 → Kubernetes 滚动更新 压缩到一台机器上的单条流水线中,每一步都有明确的配置文件和验证方法。它适合中小团队在没有专职平台工程师的情况下,用最低的运维表面积获得可重复、可追溯的发布能力。当单节点的控制面单点成为真实痛点时,再按 k3s 的演进路径扩展为三节点 etcd 高可用——那时开发者已经有了稳定的 CI/CD 流程和镜像仓库,迁移的代价是可控的。

【SOP实操】一台高配服务器,用 k3s + Gitea 从零搭建 Golang 应用的轻量级生产发布环境

https://www.wdft.com/c33dd30.html

Author

Jaco Liu

Posted on

2026-10-27

Updated on

2026-11-05

Licensed under