Seatsurfing 1.136 Docker Compose 部署指南

在一台 Ubuntu 22.04 服务器上用 Docker Compose 从零部署 Seatsurfing 1.136.3(后端 + PostgreSQL 双服务编排),完成初始化登录、持久化验证、备份与升级操作,并沉淀本次部署全程实测遇到的四个真实故障与修复过程。已在 Ubuntu 22.04 + Docker 29.8.1 + Compose v5.5.1 环境全量实测通过(ghcr.io/seatsurfing/backend:1.136.3 + postgres:18)。

Seatsurfing自托管预约调度2026/10/24

Seatsurfing 1.136 Docker Compose 部署指南

目标:在一台 Ubuntu 22.04 服务器上用 Docker Compose 从零部署 Seatsurfing 1.136.3(后端 + PostgreSQL 双服务编排),完成初始化登录、持久化验证、备份与升级操作,并沉淀本次部署全程实测遇到的四个真实故障与修复过程。已在 Ubuntu 22.04 + Docker 29.8.1 + Compose v5.5.1 环境全量实测通过(ghcr.io/seatsurfing/backend:1.136.3 + postgres:18)。

一、环境信息

本次部署实测环境(下文所有命令与回显均来自该环境):

  • 操作系统:Ubuntu 22.04(x86_64)
  • Docker:Docker version 29.8.1, build 4a63305
  • 编排工具:Docker Compose version v5.5.1
  • Seatsurfing:ghcr.io/seatsurfing/backend:1.136.3(官方 GHCR 镜像,2026-10-23 发布)
  • 数据库:postgres:18(Docker Hub 官方 PostgreSQL 镜像)
  • 宿主端口:8136(映射应用容器内 8080;示例服务器上 8080 端口另有占用,故选 8136)
  • 数据持久化:命名卷 seatsurfing-data 挂载到数据库的 /var/lib/postgresql
  • 工作目录:/opt/seatsurfing/(compose 项目目录,下文所有相对路径基于此)

系统要求(官方自托管文档标注,查询日期 2026-10-24):Linux 服务器;Docker 运行环境(实测 Docker Engine 28.x、Podman 1.4.x);PostgreSQL 16.x/17.x(本文实测用 18 亦通过,但注意 18 的数据目录布局变化,见"常见问题"第 1 条);可选 Docker Compose 2.x;推荐前置反向代理做 TLS 终结(官方实测 Traefik 2.x)。

二、镜像选型说明

当前官方形态:单镜像 + 数据库双件套。 现行官方文档(seatsurfing.io/docs/self-hosted/docker,页面更新时间 2026-10-08)的 Compose 示例只包含两个服务:

  • ghcr.io/seatsurfing/backend:Go 编写的后端,单一服务进程同时提供 REST API、员工订座界面(Booking UI,PWA)与管理后台(Admin UI),容器内统一监听 8080;
  • postgres:PostgreSQL 数据库,唯一持久化依赖。

历史注记:早期版本的官方编排曾拆出独立的前端路由镜像(Docker Hub seatsurfing/router),由它把 /ui/、/admin/ 与 API 分发给独立的 booking-ui/admin-ui 前端。截至 2026-10-24 实查,Docker Hub 的 seatsurfing 组织仅剩 backend、booking-ui、admin-ui、docs、confluence-cloud-app 五个仓库,router 仓库已不存在(接口返回 object not found),官方文档与 README 均已改为单镜像形态。新部署一律使用 ghcr.io/seatsurfing/backend 单镜像双件套,不要照搬网上旧教程的三件套编排。

镜像标签与版本对应关系(GitHub Releases 实查,2026-10-24):GHCR 上 backend 镜像标签与 Release 版本号一一对应(如 1.136.3),另有 latest 跟随最新版。生产建议固定版本号标签(如 1.136.3),升级时显式改标签,避免 latest 漂移导致不可复现。镜像体量:backend 1.136.3 约 96.7MB(distroless 基础镜像,无 shell),postgres:18 约 650MB。

三、前置条件

  1. 服务器已安装 Docker Engine 与 Docker Compose 插件(本文实测 29.8.1 / v5.5.1,官方最低要求见上一节);
  2. 服务器可访问 ghcr.io(GitHub 容器镜像仓库)与 Docker Hub;网络受限时可为 Docker 守护进程配置 HTTP 代理后再拉取;
  3. 规划好宿主端口(本文用 8136,避免与其他服务冲突:ss -ltn 确认未占用);
  4. 准备一个 32 字节的数据库字段加密密钥字符串(CRYPT_KEY,用于加密库内敏感字段;一经使用不可更换,否则已加密数据无法解密);
  5. 如需服务器重启后自动拉起,compose 中已写 restart: unless-stopped;
  6. (可选,建议)准备一对 RSA 密钥用于 JWT 签名固定化,避免每次重启后全体用户会话失效,生成方法见 4.4 节。

四、部署步骤

4.1 创建 compose 项目文件

本节要做什么:在服务器上建立 /opt/seatsurfing/ 目录并写入 docker-compose.yml,一次定义 backend 与 db 两个服务、内部网络与数据卷。预期得到一份可直接 docker compose up 的编排文件。

创建目录并编写编排文件:

mkdir -p /opt/seatsurfing/keys && cd /opt/seatsurfing

写入 docker-compose.yml(与本次实测完全一致的版本):

name: seatsurfing-1.136.3-stack

services:
  backend:
    image: ghcr.io/seatsurfing/backend:1.136.3
    container_name: seatsurfing-1.136.3-backend
    restart: unless-stopped
    depends_on:
      - db
    networks:
      - sql
    ports:
      - "8136:8080"
    environment:
      POSTGRES_URL: "postgres://seatsurfing:q1w2e3r4@db/seatsurfing?sslmode=disable"
      CRYPT_KEY: "s9Kp2xQv7Rt4Zw8Yb3Nc6Mh1Jd5Fg0La"
      PUBLIC_SCHEME: "http"
      PUBLIC_PORT: "8136"
      JWT_PRIVATE_KEY: "/app/jwt.key"
      JWT_PUBLIC_KEY: "/app/jwt.pub"
      INIT_ORG_NAME: "Quzuo Demo"
      INIT_ORG_USER: "admin"
      INIT_ORG_PASS: "q1w2e3r4"
      INIT_ORG_LANGUAGE: "en"
    volumes:
      - ./keys/jwt-trad.key:/app/jwt.key:ro
      - ./keys/jwt.pub:/app/jwt.pub:ro

  db:
    image: postgres:18
    container_name: seatsurfing-1.136.3-db
    restart: unless-stopped
    networks:
      - sql
    volumes:
      - seatsurfing-data:/var/lib/postgresql
    environment:
      POSTGRES_PASSWORD: "q1w2e3r4"
      POSTGRES_USER: "seatsurfing"
      POSTGRES_DB: "seatsurfing"

volumes:
  seatsurfing-data:

networks:
  sql:
    name: seatsurfing-1.136.3-net
    driver: bridge

逐项说明:

  • name: seatsurfing-1.136.3-stack:显式 compose 项目名。多套栈并存的服务器上务必显式命名,防止目录同名导致 down 时误删别的项目;
  • POSTGRES_URL:数据库连接串,postgres://用户:密码@服务名/库名?sslmode=disable。db 是 compose 服务名,compose 内部网络自动 DNS 解析;sslmode=disable 因为同一内部网络内明文传输(跨机部署时改用 TLS);
  • CRYPT_KEY:32 字节字符串,用于加密数据库中的敏感字段(如邮件凭据等)。首次初始化后不可更换;
  • PUBLIC_SCHEME/PUBLIC_PORT:告知应用「对外暴露的访问地址」是什么——系统发出的邮件里的链接、界面回跳都按它拼。生产走反代 TLS 时改为 PUBLIC_SCHEME=https、PUBLIC_PORT=443(默认值即 https/443);
  • JWT_PRIVATE_KEY/JWT_PUBLIC_KEY:JWT 签名密钥的 PEM 文件路径。不设置时应用每次启动生成临时密钥对,重启后所有用户会话失效(启动日志会出现 Warning,见 4.4);
  • INIT_ORG_*:首次初始化组织时的组织名、管理员用户名与密码。注意 INIT_ORG_USER 填 admin 时实际登录账号是 admin@seatsurfing.local(域部分固定,见 4.5);
  • seatsurfing-data:/var/lib/postgresql:PostgreSQL 18 镜像的新版数据目录布局要求挂载点在 /var/lib/postgresql(数据库实际落在其下 18/docker 子目录)。官方文档示例写的 /var/lib/postgresql/data 配 postgres:18 会直接拒绝启动——这是本次实测踩到的第一个坑,详见"常见问题";
  • networks.sql:显式命名的内部 bridge 网络,只有 backend 与 db 两成员,数据库不暴露宿主端口。

4.2 生成 JWT 签名密钥(建议固定)

本节要做什么:生成一对固定 RSA 密钥挂进应用,消除"每次重启会话全失效"的隐患。预期得到两个 PEM 文件且格式正确。

cd /opt/seatsurfing
# 生成 2048 位 RSA 私钥(传统 PKCS1 格式)
openssl genrsa -out keys/jwt-trad.key 2048
# 由私钥导出公钥
openssl rsa -in keys/jwt-trad.key -pubout -out keys/jwt.pub
# 放开读权限(应用进程以非 root 运行,600 权限会读失败)
chmod 644 keys/jwt-trad.key keys/jwt.pub

实测回显(head -1 查看文件首行确认格式):

$ head -1 keys/jwt-trad.key
-----BEGIN RSA PRIVATE KEY-----
$ head -1 keys/jwt.pub
-----BEGIN PUBLIC KEY-----

⚠️ 实测排障:私钥必须是 PKCS1 传统格式(BEGIN RSA PRIVATE KEY)。OpenSSL 3.x 的 openssl genrsa 默认输出 PKCS8 格式(BEGIN PRIVATE KEY),应用启动会报 x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format) 并回退临时密钥。转换方法:openssl rsa -in keys/jwt.key -traditional -out keys/jwt-trad.key。完整排障过程见"常见问题"第 3 条。

4.3 拉取镜像并启动

本节要做什么:拉取两个镜像并后台启动整栈,确认两个服务都进入 healthy/Up 状态。

cd /opt/seatsurfing
docker compose up -d

首次执行时 compose 自动拉取镜像后再启动。镜像单独拉取时(docker pull ghcr.io/seatsurfing/backend:1.136.3)实测回显(节选):

Status: Downloaded newer image for ghcr.io/seatsurfing/backend:1.136.3
ghcr.io/seatsurfing/backend:1.136.3

启动后确认状态:

docker compose ps

实测回显:

NAME                          STATUS                  PORTS
seatsurfing-1.136.3-backend   Up 10 seconds (healthy) 0.0.0.0:8136->8080/tcp, [::]:8136->8080/tcp
seatsurfing-1.136.3-db        Up About a minute       5432/tcp

backend 自带 healthcheck(镜像内置 /app/healthcheck),约半分钟内从 (health: starting) 转为 (healthy)。

查看应用启动日志:

docker logs seatsurfing-1.136.3-backend

实测回显(首次初始化,正常应当看到如下顺序):

2026/10/24 05:16:42 Starting …
2026/10/24 05:16:42 Reading config …
2026/10/24 05:16:42 Seatsurfing version 1.136.3
2026/10/24 05:16:42 Connecting to database "seatsurfing" …
2026/10/24 05:16:42 Database connection established.
2026/10/24 05:16:42 Initializing database with schema version 62 (current: 0) …
2026/10/24 05:16:42 Configuring default settings for orgs …
2026/10/24 05:16:42 Configuring default preferences for users …
2026/10/24 05:16:42 Creating default organization...
2026/10/24 05:16:42 Updating settings for primary organization …
2026/10/24 05:16:42 Initializing Public REST services …
2026/10/24 05:16:42 Public HTTP Server listening on 0.0.0.0:8080
2026/10/24 05:16:42 Seatsurfing started successfully (instance ID: 4526f3c2-bb60-47c6-a48d-490396168170)

要点:Initializing database with schema version 62 (current: 0) 表示首次启动建库(schema 62 为 1.136.3 的库结构版本);Creating default organization... 即用 INIT_ORG_* 变量创建初始组织与管理员。若启动日志出现 ⚠️ Warning: 行,按"常见问题"逐条处理。

4.4 验证各入口

本节要做什么:逐个探测根路径、订座界面、管理后台三个入口,确认服务真实可用。预期得到 307/200/307→200 的跳转链。

curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://localhost:8136/
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8136/ui/
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' http://localhost:8136/admin/
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8136/ui/admin/

实测回显:

307 http://localhost:8136/ui/
200
307 http://localhost:8136/ui/admin/
200

结论:/ 307 跳员工订座界面 /ui/;/admin/ 307 跳管理后台实际地址 /ui/admin/。浏览器访问 http://<服务器IP>:8136/ui/ 与 http://<服务器IP>:8136/ui/admin/ 即可使用。

4.5 首次登录管理后台

本节要做什么:用初始化管理员账号登录管理后台,确认组织初始化成功。预期得到登录成功并进入管理界面。

浏览器打开 http://<服务器IP>:8136/ui/admin/,登录账号:

  • 用户名:admin@seatsurfing.local(compose 中 INIT_ORG_USER: "admin",登录名固定拼上 @seatsurfing.local 域后缀;不设置该变量时默认同样是 admin@seatsurfing.local)
  • 密码:q1w2e3r4(compose 中 INIT_ORG_PASS;不设置时默认为官方文档给出的 Sea!surf1ng)

⚠️ 首次登录后请立即在管理后台右上角用户菜单改掉初始密码;INIT_ORG_PASS 只在数据库为空、首次创建组织时生效,之后改环境变量不会改密码。

登录后可见系统预置的示例数据:一个名为 "Sample Floor" 的示例楼层(含官方地图)与 17 个示例工位/会议室(实测统计接口返回 numSpaces: 17),可直接体验地图选座,再按需删除替换为自己的楼层。

五、数据持久化说明

本节说明数据都存在哪里、怎么验证、如何备份。

数据落点:全部业务数据(组织、用户、楼层、工位、预订、设置)都在 PostgreSQL 中,对应命名卷 seatsurfing-data(compose 项目名前缀,实际卷名为 seatsurfing-11363-stack_seatsurfing-data),挂载在数据库服务的 /var/lib/postgresql。backend 服务本身无状态,可随意重建。

验证持久化:重启整栈后登录与数据应原样保留。实测:

docker compose restart
docker compose ps
curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8136/ui/

实测回显:

 Container seatsurfing-1.136.3-backend  Started
 Container seatsurfing-1.136.3-db       Started
NAME                          STATUS
seatsurfing-1.136.3-backend   Up 7 seconds (healthy)
seatsurfing-1.136.3-db        Up 8 seconds
200

重启后再次登录返回 200,预订与设置均在——持久化生效。

备份:用 pg_dump 导出逻辑备份(在项目目录执行):

docker compose exec -T db pg_dump -U seatsurfing seatsurfing > seatsurfing-$(date +%F).sql

实测(导出量):319115 字节(空库刚初始化时的体量)。恢复思路:新库上执行该 SQL(docker compose exec -T db psql -U seatsurfing seatsurfing < 备份文件.sql)。定期备份建议 cron 每日一次并保留 N 份轮转。

注意:CRYPT_KEY 与备份同等重要——库中部分敏感字段用它加密,丢了密钥备份也不完整。

六、环境变量表

部署相关的核心环境变量(backend 服务;完整清单与逐项语义见《Seatsurfing 1.136 配置参数详解》):

环境变量类型默认值说明(本文实测关注点)
POSTGRES_URLstring—PostgreSQL 连接串,必填
CRYPT_KEYstring—32 字节敏感字段加密密钥,首启后不可换
PUBLIC_SCHEMEstringhttps对外访问协议;直连 HTTP 部署须显式设 http
PUBLIC_PORTint443对外访问端口;本文实测 8136
PUBLIC_LISTEN_ADDRstring0.0.0.0:8080应用监听地址与端口
INIT_ORG_NAMEstringSample Company首次初始化的组织名
INIT_ORG_USERstringadmin首次初始化的管理员用户名(登录名拼 @seatsurfing.local)
INIT_ORG_PASSstringSea!surf1ng首次初始化的管理员密码
INIT_ORG_LANGUAGEstringen组织语言(ISO 语言码)
JWT_PRIVATE_KEYstring—JWT 签名私钥 PEM 路径(PKCS1 格式,实测见排障 3)
JWT_PUBLIC_KEYstring—JWT 验签公钥 PEM 路径
DISABLE_VERSION_CHECKbool0关闭版本检查(同时关闭匿名统计)
DISABLE_ANONYMOUS_USAGE_STATSbool0关闭匿名使用统计
CACHE_TYPEstringdefault缓存:default=内置,valkey=外置 Valkey

数据库服务环境变量(postgres:18 标准变量):POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB 分别为上表 POSTGRES_URL 连接串中的用户、密码、库名。

七、配置参数说明

compose 文件中除环境变量外的关键编排参数解读:

  • restart: unless-stopped:除非手动 stop,否则随守护进程自动拉起(服务器重启后自动恢复服务);
  • depends_on: - db:仅保证启动顺序(先 db 后 backend),不等待数据库就绪——backend 自带连接重试,日志会先报连接失败再自动重连,属正常现象,无需额外 healthcheck 依赖;
  • ports: "8136:8080":宿主 8136 → 容器 8080。改动宿主端口时必须同步改 PUBLIC_PORT,否则邮件里的链接与界面回跳地址仍是旧端口;
  • volumes: ./keys/...:ro:只读挂载密钥文件,路径相对 compose 文件所在目录——源文件必须真实存在,否则 Docker 会自动创建同名目录导致应用读文件报 "is a directory"(实测排障 3);
  • networks.sql:显式命名网络 seatsurfing-1.136.3-net,数据库无宿主端口映射,只能从该网络内访问,天然隔离。

八、配置文件挂载

Seatsurfing 后端没有配置文件,全部配置经环境变量注入(官方文档明确:"global settings are set using environment variables"),因此本部署不需要挂载应用配置文件,实际挂载的只有两个 JWT 密钥 PEM:

  • ./keys/jwt-trad.key → /app/jwt.key(JWT 签名私钥,PKCS1)
  • ./keys/jwt.pub → /app/jwt.pub(JWT 验签公钥)

组织级业务配置(预订策略、默认时区、公开预订开关等)也不走文件,存放在数据库中、经管理后台修改(对应接口 GET/PUT /setting/,见《Seatsurfing 1.136 API 使用详解》)。需要修改"部署级"配置时:改 compose 的 environment: 段后 docker compose up -d 重建生效。

九、等价 docker run 命令

不用 compose 时,下列命令与上述编排文件一一等价(逐条对应 compose 中的网络、卷与环境变量;生产仍建议用 compose 管理):

# 1. 创建内部网络(对应 networks.sql)
docker network create --driver bridge seatsurfing-1.136.3-net

# 2. 启动 PostgreSQL(对应 db 服务:网络/卷/环境变量一一对应)
docker run -d \
  --name seatsurfing-1.136.3-db \
  --network seatsurfing-1.136.3-net \
  --restart unless-stopped \
  -v seatsurfing-data:/var/lib/postgresql \
  -e POSTGRES_PASSWORD=q1w2e3r4 \
  -e POSTGRES_USER=seatsurfing \
  -e POSTGRES_DB=seatsurfing \
  postgres:18

# 3. 启动 Seatsurfing 后端(对应 backend 服务;密钥文件路径按实际位置调整)
docker run -d \
  --name seatsurfing-1.136.3-backend \
  --network seatsurfing-1.136.3-net \
  --restart unless-stopped \
  -p 8136:8080 \
  -v /opt/seatsurfing/keys/jwt-trad.key:/app/jwt.key:ro \
  -v /opt/seatsurfing/keys/jwt.pub:/app/jwt.pub:ro \
  -e POSTGRES_URL='postgres://seatsurfing:q1w2e3r4@db/seatsurfing?sslmode=disable' \
  -e CRYPT_KEY=s9Kp2xQv7Rt4Zw8Yb3Nc6Mh1Jd5Fg0La \
  -e PUBLIC_SCHEME=http \
  -e PUBLIC_PORT=8136 \
  -e JWT_PRIVATE_KEY=/app/jwt.key \
  -e JWT_PUBLIC_KEY=/app/jwt.pub \
  -e INIT_ORG_NAME='Quzuo Demo' \
  -e INIT_ORG_USER=admin \
  -e INIT_ORG_PASS=q1w2e3r4 \
  -e INIT_ORG_LANGUAGE=en \
  ghcr.io/seatsurfing/backend:1.136.3

注意等价写法中 backend 用服务名 db 解析数据库(--network 同网络时 Docker 内置 DNS 生效),与 compose 行为一致。

十、常用运维命令

在 compose 项目目录(/opt/seatsurfing/)内执行:

# 查看状态(含健康检查)
docker compose ps

# 跟踪应用日志(排障第一入口)
docker compose logs -f backend

# 重启整栈(数据在卷中,不丢失)
docker compose restart

# 停止整栈(保留容器与卷)
docker compose stop

# 启动已停止的栈
docker compose start

# 检查镜像更新(不自动升级)
docker compose pull

# 升级:改 docker-compose.yml 中 backend 的镜像标签为新版本号后
docker compose up -d

# 彻底删除(含数据卷,谨慎!)
docker compose down -v

升级说明:应用启动时自动执行数据库结构迁移(本文实测 1.136.3 为 schema version 62),升级新版本后首启日志会出现 Initializing database with schema version N (current: 62) …,属正常迁移。回滚:把镜像标签改回旧版本再 up -d 即可,但若新版本已做库结构迁移,旧版应用可能提示 schema 不匹配——回滚前先用第五章的 pg_dump 备份。

单服务重建(只改了 backend 环境变量时,不动数据库):

docker compose up -d --force-recreate backend

十一、常见问题(实测排障实录)

以下四个问题全部来自本次部署的真实故障,回显逐行照抄,供直接对号入座。

11.1 数据库反复重启(Restarting),报 "unused mount/volume"

现象:up -d 后 db 容器进入 Restarting (1) 循环,backend 因连不上库退出(Exited(2))。docker logs seatsurfing-1.136.3-db 实测回显:

Error: in 18+, these Docker images are configured to store database data in a
       format which is compatible with "pg_ctlcluster" (specifically, using
       major-version-specific directory names).  This better reflects how
       PostgreSQL itself works, and how upgrades are to be performed.

       See also https://github.com/docker-library/postgres/pull/1259

       Counter to that, there appears to be PostgreSQL data in:
         /var/lib/postgresql/data (unused mount/volume)

       This is usually the result of upgrading the Docker image without
       upgrading the underlying database using "pg_upgrade" (which requires both
       versions).

       The suggested container configuration for 18+ is to place a single mount
       at /var/lib/postgresql which will then place PostgreSQL data in a
       subdirectory, allowing usage of "pg_upgrade --link" without mount point
       boundary issues.

       See https://github.com/docker-library/postgres/issues/37 for a (long)
       discussion around this process, and suggestions for how to do so.

定位:postgres:18 镜像把默认数据目录改为 /var/lib/postgresql/<主版本>/docker(PGDATA 不再是 /var/lib/postgresql/data)。18 镜像的入口脚本在检测到 /var/lib/postgresql/data 被挂载(即使是全新空卷)且新 PGDATA 不在其中时,直接报错拒绝启动——换言之,官方 Seatsurfing 文档里 db:/var/lib/postgresql/data 的示例配 postgres:18 在全新部署下也会触发此错。

解决:把卷挂载点上移一层,挂 /var/lib/postgresql(数据实际落在卷内 18/docker/ 子目录):

    volumes:
      - seatsurfing-data:/var/lib/postgresql

重建后实测 db 正常进入 Up 状态,数据全部落在该卷。若坚持旧挂载点,也可显式指定子目录 PGDATA: /var/lib/postgresql/data/pgdata,但官方推荐前者(利于后续 pg_upgrade 原地升级)。

11.2 backend 起不来:端口已被占用

现象:docker compose up -d 报错(实测回显):

Error response from daemon: failed to set up container networking: driver failed programming external connectivity on endpoint seatsurfing-1.136.3-backend (...): Bind for :::8128 failed: port is already allocated

定位:宿主 8128 端口已被其他进程/容器占用(ss -ltn | grep 8128 可确认)。

解决:换一个未占用宿主端口,同时改两处——ports 映射与 PUBLIC_PORT 环境变量(两者必须一致),然后 docker compose up -d。

11.3 JWT 密钥三连坑:is a directory → permission denied → PKCS 格式

现象:为消除"每次重启会话失效"配置 JWT 密钥时,依次踩到三个真实报错(docker logs seatsurfing-1.136.3-backend 实测回显):

第一坑——挂载源文件不存在,Docker 自动创建同名目录:

⚠️  Warning: Loading private key failed read /app/jwt.key: is a directory
⚠️  Warning: Loading public key failed read /app/jwt.pub: is a directory
⚠️  Warning: No valid JWT_PRIVATE_KEY or JWT_PUBLIC_KEY set. Generating a temporary random private/public key pair …

原因:compose 里 ./keys/jwt.key 相对路径在 compose 文件所在目录下不存在,创建容器时 Docker 把它当作待挂载路径自动建成了目录。解决:确认密钥文件真实存在于 compose 目录的相对路径下(ls -l keys/ 核对),重建容器。

第二坑——权限不足,应用进程非 root 读不了 600 的私钥:

⚠️  Warning: Loading private key failed open /app/jwt.key: permission denied

解决:chmod 644 keys/jwt-trad.key keys/jwt.pub(distroless 镜像内无 shell,直接在宿主侧改挂载源权限)。

第三坑——私钥格式不对,应用只认 PKCS1:

⚠️  Warning: Loading private key failed x509: failed to parse private key (use ParsePKCS8PrivateKey instead for this key format)

原因:OpenSSL 3.x genrsa 默认输出 PKCS8(BEGIN PRIVATE KEY),而应用按 PKCS1(BEGIN RSA PRIVATE KEY)解析。解决:openssl rsa -in jwt.key -traditional -out jwt-trad.key 转传统格式后重建。三处修复后实测启动日志再无任何 Warning。

11.4 忘记/搞错初始管理员登录名

现象:用 admin 或 compose 里 INIT_ORG_USER 的值登录失败。

定位:登录名不是该变量原值,而是固定拼上本地域后缀。实测系统组织信息接口返回的组织域为 localhost,初始账号登录名为 admin@seatsurfing.local(不设置 INIT_ORG_USER 时默认同此)。

解决:用 admin@seatsurfing.local + INIT_ORG_PASS(默认 Sea!surf1ng)登录。若密码也丢失且库中无可用的其他管理员,只能在数据可弃的前提下 docker compose down -v 清卷重新初始化(数据可弃时才可用;否则从备份恢复)。

11.5 初始化组织后改 INIT_ORG_* 不生效

定位:INIT_ORG_* 仅在"指定数据库中尚无任何组织"的首启时生效(官方文档:默认仅当库里没有组织时才创建初始组织)。改这些变量后重启不会有任何变化。

解决:组织设置在管理后台改(语言、名称等);只有清库重建才能重新触发初始化。

十二、参考链接