文章

在 1Panel 上部署自建博客系统:踩过的 12 个坑

在 1Panel 上部署自建博客系统:踩过的 12 个坑 一台 2 核 4G 的轻量云服务器,Ubuntu 22.04 + 1Panel + Docker Compose, 部署一个 Go(Gin) + Nuxt 3 + MySQL 8.4 的双容器博客。 本地开发机跑得好好的,上服务器后连着撞了 12 个问题。…

在 1Panel 上部署自建博客系统:踩过的 12 个坑
目录 · 17 节

在 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 可用"的真实部署之后。

评论

评论需要先审核,通过后才会显示

评论暂时无法加载。