上班也有三年了,手里做过的项目慢慢多了起来。

之前代码基本都是散着放,时间长了以后越来越觉得,自己还是有必要弄一个专门的代码仓库。
并且所有的数据都只存在公司的服务器,也让我觉得没有安全感。
GitHub、码云之类的公共平台当然很好用,但对我来说总感觉不是特别适合存放一些私人项目,而且部分平台对单文件大小、仓库容量之类也会有一些限制。
而我的需求其实很简单:
版本管理
代码备份
能在外面访问
最好完全由自己控制
所以最后还是决定,在家里的服务器上自建一个 Git 服务。
为什么选择 Gitea
关于 GitLab 和 Gitea 的对比,网上已经有很多文章了,这里就不再赘述。
我最后选择 Gitea 的理由也非常简单:
够轻量,而且功能完全满足我的需求。
我的服务器上本身安装了 1Panel,所以部署 Gitea 基本没什么难度,直接在应用商店里一键安装即可。
本质上也是通过 Docker Compose 来部署。

安装完成以后,在内网里直接访问对应 IP 和端口就可以进入 Gitea。
具体的安装配置没什么特别的,这里就不再单独贴截图了。
接下来真正需要解决的问题是:
怎么让这个运行在家里的 Gitea,在外网也能访问。
使用 SakuraFrp 进行内网穿透
我之前已经在家里的软路由 iKuai 上安装了 SakuraFrp,所以这里就直接继续用它。
另外,我注册了一个域名:
usagi.ink
并且已经完成备案。
这次准备用:
git.usagi.ink
作为私人 Git 服务的访问域名。
当然,如果不想使用自己的域名,也可以直接使用 SakuraFrp 分配的穿透地址。
功能上没有什么区别。
就是看着多少有点不太优雅(

创建 Gitea 的 HTTP 隧道
首先进入 SakuraFrp 后台,新建一条隧道。

在 本地 IP 和 本地端口 中填写 Gitea 在局域网中的访问地址。
同时,在自动 HTTPS 的域名位置填写:
git.usagi.ink
需要注意的是,自己的域名要提前在 SakuraFrp 后台完成过白,否则无法用于穿透。

配置完成后,重启 SakuraFrp 客户端,并启动刚刚创建的隧道。
随后查看运行日志,可以找到 SakuraFrp 分配给这条隧道的域名。
将自己的:
git.usagi.ink
添加一条 CNAME 解析,指向日志中提供的域名即可。


做到这里以后,实际上就已经可以通过自己的域名访问 Gitea 了。
不过现在还有一个问题:
SSL 证书。
给域名申请自己的 SSL 证书
此时直接访问的话,使用的是 SakuraFrp 自动生成的证书,浏览器可能会提示证书不受信任。
所以接下来还需要给:
git.usagi.ink
单独申请一张自己的 SSL 证书。
因为我使用的 1Panel 本身就带有证书管理功能,所以这里直接使用 1Panel 来申请和管理证书。
首先需要创建:
ACME 账户
DNS 账户
ACME 账户主要用于申请证书,DNS 账户则用于自动完成 DNS 验证。
ACME 账户配置很简单,填写自己的邮箱即可。
DNS 账户则需要根据域名当前使用的 DNS 服务商来选择。
我这里已经把域名接入了阿里云 ESA,所以使用对应的阿里云账户配置。
然后进入阿里云后台,获取对应的:
Access Key
Secret Key
填写进去即可。


在 1Panel 中申请证书
接下来点击申请证书。
填写需要申请的域名:
git.usagi.ink
然后依次选择:
刚刚创建的 ACME 账户
DNS 验证
对应的 DNS 账户
同时记得勾选:
自动续签
这样以后证书即将过期时,1Panel 就会自动使用刚刚配置的账户重新申请证书,不需要再手动操作。

接着继续往下翻。
勾选:
推送证书到本地目录
然后选择一个用于保存证书的路径。
同时再勾选:
申请证书后执行脚本

这里为什么需要执行脚本,就要说到 SakuraFrp 的证书加载方式了。
将证书同步到 SakuraFrp
证书申请完成后,我们需要将:
fullchain.pem
重命名为:
域名.crt
同时将:
privkey.pem
重命名为:
域名.key
以我这里的域名为例,也就是:
git.usagi.ink.crt
git.usagi.ink.key
然后将这两个文件复制到 SakuraFrp 的工作目录:
FrpcWorkingDirectory
之后重启隧道,SakuraFrp 就会自动加载这套证书。
手动操作的话,到这里其实就已经够用了。
但问题也很明显。
以后每次证书续签,都得再手动复制一次。
既然 1Panel 本身就支持在证书申请完成以后执行脚本,那自然没必要每次自己去折腾。
于是我让 Codex 帮我写了一个自动同步证书的脚本。
自动同步证书脚本
我的 SakuraFrp 是运行在 iKuai 上的,因此这里通过 SMB 共享,将 SakuraFrp 的工作目录暴露出来。
脚本负责:
临时挂载 iKuai 的 SMB 共享;
读取 1Panel 生成的新证书;
在临时目录中将证书重命名为 SakuraFrp 所需的格式;
将证书同步到
FrpcWorkingDirectory;校验证书文件是否复制成功;
最后自动卸载 SMB。
脚本如下:
#!/usr/bin/env bash
set -Eeuo pipefail
SOURCE_DIR="/opt/1panel/Certificate/git"
SMB_SHARE="//10.0.0.1/ikuai_skfrp"
MOUNT_POINT="/mnt/ikuai-skfrp"
TARGET_DIR="${MOUNT_POINT}/Docker-config/sakurafrp/FrpcWorkingDirectory"
CREDENTIAL_FILE="/root/.ikuai-skfrp-credentials"
LOCK_FILE="/run/lock/push-cert-to-sakurafrp.lock"
DOMAIN_NAME="git.usagi.ink"
TARGET_KEY="${DOMAIN_NAME}.key"
TARGET_CERT="${DOMAIN_NAME}.crt"
mounted_here=0
staging_dir=""
log() {
printf '[%s] %s\n' "$(date '+%F %T')" "$*"
}
cleanup() {
if [[ -n "${staging_dir}" && -d "${staging_dir}" ]]; then
rm -rf -- "${staging_dir}"
fi
if [[ "${mounted_here}" -eq 1 ]] && mountpoint -q "${MOUNT_POINT}"; then
log "卸载临时 SMB 挂载"
timeout 15 umount "${MOUNT_POINT}" ||
umount -l "${MOUNT_POINT}" ||
true
fi
}
trap cleanup EXIT
# 防止多个证书任务同时运行
mkdir -p "$(dirname "${LOCK_FILE}")"
exec 9>"${LOCK_FILE}"
if ! flock -n 9; then
log "已有证书推送任务正在运行,本次退出"
exit 0
fi
# 检查源目录
if [[ ! -d "${SOURCE_DIR}" ]]; then
log "错误:证书目录不存在:${SOURCE_DIR}"
exit 1
fi
if [[ ! -f "${SOURCE_DIR}/privkey.pem" ]]; then
log "错误:找不到私钥:${SOURCE_DIR}/privkey.pem"
exit 1
fi
if [[ ! -f "${SOURCE_DIR}/fullchain.pem" ]]; then
log "错误:找不到完整证书链:${SOURCE_DIR}/fullchain.pem"
exit 1
fi
# 检查凭据文件
if [[ ! -f "${CREDENTIAL_FILE}" ]]; then
log "错误:SMB 凭据文件不存在:${CREDENTIAL_FILE}"
exit 1
fi
if [[ "$(stat -c '%a' "${CREDENTIAL_FILE}")" != "600" ]]; then
log "错误:SMB 凭据文件权限必须是 600"
exit 1
fi
# 在临时目录准备待推送文件,避免修改 1Panel 原始证书
staging_dir="$(mktemp -d /tmp/push-sakurafrp-cert.XXXXXX)"
chmod 700 "${staging_dir}"
log "准备证书文件"
# 复制所有文件,并跟随可能存在的符号链接
cp -aL "${SOURCE_DIR}/." "${staging_dir}/"
# 只在临时目录中重命名
mv -- \
"${staging_dir}/privkey.pem" \
"${staging_dir}/${TARGET_KEY}"
mv -- \
"${staging_dir}/fullchain.pem" \
"${staging_dir}/${TARGET_CERT}"
# 私钥仅允许 root 读取
chmod 600 "${staging_dir}/${TARGET_KEY}"
chmod 644 "${staging_dir}/${TARGET_CERT}"
mkdir -p "${MOUNT_POINT}"
# 如果尚未挂载,则临时挂载
if ! mountpoint -q "${MOUNT_POINT}"; then
log "挂载 iKuai SMB 共享"
timeout 30 mount -t cifs \
"${SMB_SHARE}" \
"${MOUNT_POINT}" \
-o "credentials=${CREDENTIAL_FILE},vers=2.0,sec=ntlmssp,iocharset=utf8,noserverino,uid=0,gid=0,file_mode=0600,dir_mode=0700"
mounted_here=1
fi
# 防止挂载失败后误写入本地目录
filesystem_type="$(findmnt -n -o FSTYPE --target "${MOUNT_POINT}")"
if [[ "${filesystem_type}" != "cifs" ]]; then
log "错误:${MOUNT_POINT} 不是 CIFS 挂载点,拒绝复制"
exit 1
fi
mounted_source="$(findmnt -n -o SOURCE --target "${MOUNT_POINT}")"
if [[ "${mounted_source}" != "${SMB_SHARE}" ]]; then
log "错误:挂载点连接的是其他共享:${mounted_source}"
exit 1
fi
mkdir -p "${TARGET_DIR}"
log "开始推送证书到:${TARGET_DIR}"
timeout 300 rsync \
-rLt \
--checksum \
--no-owner \
--no-group \
--no-perms \
--omit-dir-times \
--partial \
--partial-dir=".rsync-partial" \
--delay-updates \
--itemize-changes \
"${staging_dir}/" \
"${TARGET_DIR}/"
# 校验关键文件是否完整
if ! cmp -s \
"${staging_dir}/${TARGET_KEY}" \
"${TARGET_DIR}/${TARGET_KEY}"; then
log "错误:私钥复制后校验失败"
exit 1
fi
if ! cmp -s \
"${staging_dir}/${TARGET_CERT}" \
"${TARGET_DIR}/${TARGET_CERT}"; then
log "错误:证书复制后校验失败"
exit 1
fi
log "证书推送和校验完成"
log "私钥:${TARGET_DIR}/${TARGET_KEY}"
log "证书:${TARGET_DIR}/${TARGET_CERT}"这样设置以后,每次 1Panel 自动续签证书,脚本都会跟着执行。
新的证书会自动同步到 SakuraFrp 的工作目录中。

也就是说,只要第一次配置完成,以后的证书续签基本就不需要再人工干预了。
到这里,Gitea 的 HTTPS 访问部分就算配置完成。
配置 Git SSH
如果只是通过网页访问 Gitea,到上一步其实已经结束了。
但平时使用 Git 的时候,我还是更习惯通过 SSH 进行 Push 和 Pull。
因此还需要在 SakuraFrp 中额外创建一条 TCP 隧道。
在隧道配置中填写:
Gitea 所在服务器的 IP
Gitea 的 SSH 端口
这里需要注意:
外部端口和内部端口需要保持一致。

创建完成以后,和前面的 HTTP 隧道一样,从 SakuraFrp 日志中找到对应的域名。
然后再给 SSH 使用的域名添加一条 CNAME 记录。

修改 Gitea 的 SSH 配置
隧道建好以后,还需要修改 Gitea 本身的配置文件。
首先将 Gitea 中配置的 SSH 端口修改为刚刚在 SakuraFrp 中填写的端口。
同时不要忘记修改域名。
否则以后在 Gitea 仓库页面中直接复制出来的 Clone 地址还是原来的地址,每次都得自己手动修改。


最后,容器本身对应的端口配置也不要忘记修改。

修改完成并重新加载配置以后,网页访问和 Git SSH 基本就都可以正常使用了。
最后
到这里,我这套 Gitea 的内网穿透就算彻底配置完成了。
最后的结构大概就是:
同时 SSL 证书也通过:
实现了自动更新。
对于我这种需求比较简单、只是想给自己弄一个私人代码仓库的人来说,目前这套方案用起来还是挺舒服的。
平时在家里可以直接走内网,在外面则通过 SakuraFrp 访问。
证书续签也不用再手动管。
最后再提醒一句:
如果 Gitea 只是自己使用,记得关闭新用户注册。
毕竟都已经叫私人 Git 了,就没必要在公网给自己多留一个入口了(
评论