在 1Panel 上部署自建博客系统:踩过的 12 个坑
一台 2 核 4G 的轻量云服务器,Ubuntu 22.04 + 1Panel + Docker Compose,
部署一个 Go(Gin) + Nuxt 3 + MySQL 8.4 的双容器博客。
本地开发机跑得好好的,上服务器后连着撞了 12 个问题。
这篇文章把每个问题都按「报错原文 → 根因 → 改法」写清楚,
其中有 2 个是项目自身的 bug——任何人在空数据库上部署都会遇到。
先说结论
| 类别 | 数量 | 典型症状 |
|---|---|---|
| 环境差异 | 7 | 本地能过、服务器报"找不到包/文件/命令" |
| 项目自身的 bug | 2 | 空库首次启动直接崩、迁移 SQL 语法错误 |
| 部署配置 | 3 | 网络不通、执行目录错、改了配置没生效 |
如果你只想看最有价值的:跳到 第 8 条(顺序 bug)
和 第 9 条(注释导致的语法错误)——
这两个是代码问题,跟环境无关。
一、环境差异(7 个)
1. Go 版本对不上
go: go.mod requires go >= 1.26.0 (running go 1.24.13; GOTOOLCHAIN=local)
target api: failed to solve: process "/bin/sh -c go env -w GOPROXY=… && go mod download"
did not complete successfully: exit code: 1
根因:go.mod 写的是 go 1.26.0(本地用 1.27 执行过 go mod tidy),
而 Dockerfile 的构建镜像是 golang:1.24-alpine,且 GOTOOLCHAIN=local
不允许构建时自动下载工具链。
改法:构建镜像升到与 go.mod 一致。
-FROM golang:1.24-alpine AS builder
+FROM golang:1.26-alpine AS builder
为什么不把
go.mod降到 1.24?因为无法在本地验证 1.24 下所有依赖都编得过。
升镜像是确定的,降级才是赌博。
2. 服务器连不上 Alpine 源
[stage-1 2/5] RUN apk add --no-cache ca-certificates tzdata
WARNING: fetching https://dl-cdn.alpinelinux.org/alpine/v3.20/main: Permission denied
ERROR: unable to select packages:
ca-certificates (no such package): required by: world[ca-certificates]
根因:服务器访问不了 Alpine 官方源,运行阶段装不上 CA 证书。
改法:运行阶段真正需要的只有 CA 证书,从 builder 里拷,不依赖网络。
-FROM alpine:3.20
-RUN apk add --no-cache ca-certificates tzdata && adduser -D -u 10001 appuser
+COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt
+RUN adduser -D -u 10001 appuser
adduser 是 busybox 自带命令,不装包也能用。
3. Alpine 镜像里没有 /usr/share/zoneinfo
failed to compute cache key: "/usr/share/zoneinfo": not found
根因:Alpine 的时区数据是独立的包(tzdata),基础镜像里没有。
改法:让二进制自带时区库(Go 1.15+ 的 time/tzdata,约 +450KB)。
// cmd/server/main.go
import (
_ "time/tzdata" // 时区数据编进可执行文件,TZ=Asia/Shanghai 才生效
)
4. .npmrc 里写着开发机的绝对路径
store-dir=/data/dsh/home/chenzhanxu/.pnpm-store
cache-dir=/data/dsh/home/chenzhanxu/.pnpm-cache
根因:容器里没有这个目录,pnpm 尝试创建 → permission denied。
看起来像"磁盘权限问题",其实是配置写死了开发机路径。
改法:删掉这两行(用 pnpm 默认位置),并换成国内 registry。
registry=https://registry.npmmirror.com
strict-peer-dependencies=false
auto-install-peers=true
enable-pre-post-scripts=true
5–7. pnpm 拒绝执行依赖的安装脚本(连续踩三次)
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: esbuild@0.28.2
Run "pnpm approve-builds" to pick which dependencies should be allowed to run scripts.
根因:pnpm 10+ 出于供应链安全,默认不执行依赖的 postinstall 脚本;esbuild 正是靠它落地二进制,不跑就 exit 1。开发机本地执行过交互式approve-builds,所以从来没暴露。
我在这里连续错了三次,值得记下来:
| 尝试 | 写法 | 结果 |
|---|---|---|
| ① | package.json 的 pnpm.onlyBuiltDependencies |
✗ 被忽略 |
| ② | pnpm-workspace.yaml 里 allowBuilds: |
✗ pnpm 没有这个键 |
| ③ | pnpm-workspace.yaml 里 onlyBuiltDependencies: |
✗ 仍报同样的错 |
| ④ | 命令行传参 | ✓ 生效 |
pnpm 自己其实给过提示:
[WARN] The "pnpm" field in package.json is no longer read by pnpm.
The following keys were ignored: "pnpm.onlyBuiltDependencies".
改法:不再猜"哪个文件生效",直接在命令上传(优先级最高)。
RUN pnpm install --frozen-lockfile=false \
--config.onlyBuiltDependencies=esbuild \
--config.strictDepBuilds=false
strictDepBuilds=false 是保险:即使白名单仍被忽略,install 也不会
因为"有依赖被跳过构建"而退出 1。Linux 上 esbuild 用的是optionalDependencies 里的平台包(@esbuild/linux-x64),
不跑 postinstall 也能用。
教训:当一个配置"写哪儿都不生效"时,别继续试文件位置——
换成命令行参数,直接绕开这层不确定性。
二、项目自身的 bug(2 个,空库必现)
8. 补列(ALTER)跑在建表之前
{"msg":"已连接数据库","driver":"mysql"}
{"level":"ERROR","msg":"服务启动失败",
"error":"补列 media.post_id: Error 1146 (42S02):
Table 'chenzhanxu_blog.media' doesn't exist"}
根因:Migrate() 里 ensureMediaScopeColumns()(alter table media add column)
排在基线判断和建表迁移之前。空库上 media 表还不存在,alter 直接报错,
整个服务起不来。开发机的表是几个月前建的,所以这个顺序问题一直没暴露。
改法:把补列移到「跑完迁移 / 按基线跳过」之后——此时表一定存在。
-ensureMediaScopeColumns(ctx, db) // 太早:表还没建
applied, err := appliedVersions(ctx, db)
// …基线判断 + 逐条执行建表迁移…
+ensureMediaScopeColumns(ctx, db) // 建表之后再补列,幂等
这个改法我还插错过一次:第一次"移动"插到了基线循环之后、建表循环之前,
错误一模一样。教训:移动代码必须用grep -n核对行号顺序,
不能只看"补丁应用成功"。
9. 迁移 SQL 里的注释导致 1064
第 16 条语句失败: Error 1064 (42000): You have an error in your SQL syntax;
near '--(不设外键:media.id 在回收站里也还在,级联删掉封面没必' at line 15
create table media_albums (
根因:迁移文件里写的是 --(中文说明)。
MySQL 的 -- 注释要求后面跟空格,--( 不是注释;
而拆分器 splitStatements 把注释原样留在语句里,
整条 create table 发给 MySQL 就成了语法错误。
改法:拆分时丢弃行注释与块注释,语句里只保留真正的 SQL。
case inLine:
- cur.WriteRune(c) // 注释被写进语句 → 发给 MySQL
if c == '\n' {
inLine = false
}
验证方式:用 root 临时建一个空库,跑一遍 sqlstore.Migrate:
✓ 建了空库
✓ Migrate 成功 applied=[1]
✓ 建表 39 张
✓ media 的 post_id/dir 列:2/2
✓✓ 空库启动路径验证通过
✓ 测试库已删除
三、部署配置(3 个)
10. 编排没加入 1Panel 的网络
dial tcp MySQL:3306: connect: connection refused
根因:1Panel 管理的 MySQL 容器名是 MySQL,但 api 容器和它
不在同一个 Docker 网络,容器名解析不了。
改法:编排里解开 1panel-network(external: true),api 加入该网络,DB_DSN 主机填容器名。
services:
api:
networks:
- 1panel-network
networks:
1panel-network:
external: true
DB_DSN=*****:***@tcp(MySQL:3306)/chenzhanxu_blog?charset=utf8mb4&parseTime=true&loc=Local
11. docker compose 在错误目录执行
root@localhost:~# docker compose up -d
no configuration file provided: not found
根因:在 /root 下执行,找不到 compose 文件。
改法:
cd /home/chenzhanxu/chenzhanxu/deploy/1panel
docker compose up -d
12. 改了端口却没生效
curl: (7) Failed to connect to 127.0.0.1 port 8010 after 0 ms: Connection refused
根因:改了 .env 的 WEB_PORT=8010,但没重新 up -d,容器还绑着旧端口。
改法:
grep '^WEB_PORT' .env # 确认 WEB_PORT=8010
docker compose up -d # 重建 web 容器,让端口映射生效
docker compose ps # 期望:127.0.0.1:8010->3000/tcp
四、成功的样子
[+] up 2/2
✔ Container czx-api Healthy
✔ Container czx-web Started
{"msg":"已连接数据库","driver":"mysql"}
{"msg":"执行迁移","version":1,"name":"init"}
{"msg":"迁移完成","applied":[1]}
{"msg":"后端已启动","addr":"0.0.0.0:8080"}
{"method":"GET","path":"/healthz","status":200}
HTTPS 侧(1Panel 反向代理 + Let's Encrypt 证书之后):
HTTP/2 200
server: openresty
x-content-type-options: nosniff
x-frame-options: DENY
referrer-policy: strict-origin-when-cross-origin
permissions-policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
cross-origin-opener-policy: same-origin
strict-transport-security: max-age=31536000; includeSubDomains
五、什么时候该怀疑"环境",什么时候该怀疑"代码"
| 现象 | 更可能是 |
|---|---|
| 本地能过、服务器某一步报"找不到包 / 文件 / 命令" | 环境差异(网络、镜像、工具链) |
| 报错提到"表不存在 / 语法错误 / 顺序" | 代码 bug(尤其空库首次部署) |
| "配置写哪儿都不生效" | 换命令行参数,别跟配置文件较劲 |
构建日志显示 CACHED 但你明明改了代码 |
先 git log 确认提交,再 --no-cache |
六、给要部署的人三条建议
1. 一定要在"空数据库"上演练一次。
这次暴露的两个产品 bug,都是因为开发库早就建好、迁移早已"已应用"才被藏起来的。
本地跑通 ≠ 空库能起。
2. 构建缓存会骗你。
COPY . . 显示 CACHED 却报旧错误时,
先 git log --oneline -1 + grep -n 核对源码,再 --no-cache 重建。
3. 部署完立刻改默认口令。
首次启动会把初始密码打进日志:
WARN 用户表是空的,已创建首个管理员(请尽快在设置页改密码)
username=***** initial_password=********
这行日志,任何能看到容器日志的人都看得到。
写于 2026 年 10 月,一次从"本地能跑"到"公网 HTTPS 可用"的真实部署之后。
