Kutt 3.2.6 Docker Compose 部署指南

Ubuntu 24.04 服务器上用 Docker Compose 部署 Kutt 3.2.6 短链服务全实录:官方四变体与预构建镜像选型、Postgres16+Redis7 三服务编排、首启建管与 API 全功能实测、数据持久化验证、环境变量表与等价 docker run,全部命令回显实测。

KuttKutt自托管DockerNode.jsPostgreSQL2026/11/02

Kutt 3.2.6 Docker Compose 部署指南

目标:在 Ubuntu 24.04 服务器上以 Docker Compose 从零部署 Kutt v3.2.6(官方镜像 + PostgreSQL 16 + Redis 7 三服务栈),完成首启建管理员、API 生成短链、跳转与统计的端到端验证;全部步骤已在真实环境实测通过(2026-11-01),命令与回显均为真实运行结果。功能与选型背景见 《Kutt 3.2.6 介绍与选型指南》。

一、环境信息

本文部署过程在以下环境实测:

  • 操作系统:Ubuntu 24.04.5 LTS(x86_64)
  • Docker 版本:29.9.2(Docker Compose v5.6.1 插件)
  • Kutt 版本:v3.2.6(官方镜像 kutt/kutt:v3.2.6,镜像 ID fa3d24a89b04,本地解压 491MB / 内容层 107MB)
  • 数据库:postgres:16-alpine;缓存与队列:redis:7-alpine
  • 部署目录:/opt/kutt(compose 项目与持久化数据根)
  • 服务端口:应用内 3000;宿主机映射 8153;Postgres/Redis 不对外暴露,仅走 compose 内部网络
  • 管理员账号:admin@kutt.local / q1w2e3r4(本文首启实测创建)

二、镜像选型说明

Kutt 官方仓库 thedevs-network/kutt(注意旧组织名 thedevs-kutt/kutt 已 404)在根目录提供 4 个 compose 变体,实测核对(GitHub raw 拉取):

文件数据库Redis说明
docker-compose.ymlSQLite(默认)无官方默认,单服务最简形态
docker-compose.sqlite-redis.ymlSQLite有加缓存与访问队列
docker-compose.postgres.ymlPostgreSQL有生产推荐(本文采用其编排)
docker-compose.mariadb.ymlMariaDB 10有MySQL 生态团队适用

四个变体的 server 服务全部写的是 build: context .(从源码目录本地构建),意味着官方 compose 假设你先 clone 整个仓库。更省事的生产路径是直接用 Docker Hub 预构建镜像:官方 release workflow(.github/workflows/docker-build-release.yaml)在每个 Release 发布时自动构建并推送 kutt/kutt:<tag> 与 kutt/kutt:latest(linux/amd64 + linux/arm64 双架构)。本文采用 image: kutt/kutt:v3.2.6 固定版本(便于回滚与复现),编排结构对齐官方 postgres 变体。

镜像内部构成实测(docker inspect):

docker inspect kutt/kutt:v3.2.6 \
  --format 'Entrypoint={{.Config.Entrypoint}} Cmd={{.Config.Cmd}} WorkingDir={{.Config.WorkingDir}} ExposedPorts={{.Config.ExposedPorts}}'
Entrypoint=[docker-entrypoint.sh] Cmd=[/bin/sh -c npm run migrate && npm start] WorkingDir=/kutt ExposedPorts=map[3000/tcp:{}]

基座为 node:22-alpine(Node.js 22.23.1),启动命令自带数据库迁移(npm run migrate 即 knex migrate:latest),首次启动自动建表、升级时自动补迁移,无需手工初始化数据库。

镜像拉取实测(官方 Docker Hub 直连可用,未发生渠道降级):

docker pull kutt/kutt:v3.2.6
Digest: sha256:fa3d24a89b04e0feb542ed7d3c77993d58dbcc1406e5291a369595d543886146
Status: Downloaded newer image for kutt/kutt:v3.2.6

三、前置条件

部署前确认三项:Docker 运行时版本、宿主机 8153 端口空闲、持久化目录可写。

docker --version && docker compose version   # 确认运行时版本
ss -tln | grep :8153 || echo "8153 free"     # 确认端口未被占用
mkdir -p /opt/kutt/data/pg /opt/kutt/data/redis /opt/kutt/custom   # 持久化目录
Docker version 29.9.2, build a82e528
Docker Compose version v5.6.1
8153 free

⚠️ 注意:data/pg、data/redis 需对容器内 uid 可写(postgres 镜像以 postgres 用户运行、redis 以 root 运行,默认 bind mount 权限即可);custom 目录用于挂载自定义静态资源(logo、CSS,见第七章)。

四、部署步骤

4.1 准备环境变量文件

在 /opt/kutt 下创建 .env(compose 自动读取)。JWT_SECRET 必须为随机长字符串,用 heredoc 现场生成:

cd /opt/kutt
cat > .env <<EOF
JWT_SECRET=$(openssl rand -hex 32)
DB_NAME=kutt
DB_USER=postgres
DB_PASSWORD=q1w2e3r4
EOF
chmod 600 .env
cat .env
JWT_SECRET=9f2c…(64 位十六进制随机串,实测值已脱敏)
DB_NAME=kutt
DB_USER=postgres
DB_PASSWORD=q1w2e3r4

⚠️ 注意:生产环境请把 DB_PASSWORD 换成强口令并同步修改 Postgres 服务的 POSTGRES_PASSWORD;示例值仅为演示账号规范。JWT_SECRET 一旦投入生产不可更换——更换会使所有已签发 JWT 与登录态失效。

4.2 编写 compose 编排文件

/opt/kutt/docker-compose.yml(实测全文):

services:
  server:
    image: kutt/kutt:v3.2.6
    container_name: kutt-3.2.6-server
    restart: always
    volumes:
      - ./custom:/kutt/custom
    environment:
      PORT: 3000
      SITE_NAME: Kutt
      DEFAULT_DOMAIN: localhost:8153
      JWT_SECRET: ${JWT_SECRET}
      DB_CLIENT: pg
      DB_HOST: postgres
      DB_PORT: 5432
      DB_NAME: ${DB_NAME}
      DB_USER: ${DB_USER}
      DB_PASSWORD: ${DB_PASSWORD}
      REDIS_ENABLED: "true"
      REDIS_HOST: redis
      REDIS_PORT: 6379
      DISALLOW_REGISTRATION: "false"
      DISALLOW_ANONYMOUS_LINKS: "false"
      TRUST_PROXY: "true"
    ports:
      - "8153:3000"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
  postgres:
    image: postgres:16-alpine
    container_name: kutt-3.2.6-postgres
    restart: always
    volumes:
      - ./data/pg:/var/lib/postgresql/data
    environment:
      POSTGRES_DB: ${DB_NAME}
      POSTGRES_USER: ${DB_USER}
      POSTGRES_PASSWORD: ${DB_PASSWORD}
    expose:
      - 5432
    healthcheck:
      test: ["CMD", "pg_isready"]
      interval: 10s
      timeout: 5s
      retries: 5
  redis:
    image: redis:7-alpine
    container_name: kutt-3.2.6-redis
    restart: always
    volumes:
      - ./data/redis:/data
    expose:
      - 6379

关键设计逐项说明:

  • DB_CLIENT: pg:knex 数据库驱动选择,可换 better-sqlite3(默认)或 mysql2;
  • DEFAULT_DOMAIN: localhost:8153:决定 API 返回的短链前缀与"不允许缩短本站链接"的校验域名,上线后必须改为对外域名;
  • depends_on.postgres.condition: service_healthy:等 pg_isready 通过再起应用,避免迁移连不上库;
  • DISALLOW_REGISTRATION/DISALLOW_ANONYMOUS_LINKS: "false":实测默认均为 true(禁注册、禁匿名建链),演示场景放开;生产多用户场景注意注册还需 SMTP(见 4.5);
  • TRUST_PROXY: "true":置于 Nginx 反代后取真实访客 IP;直连部署应设 false,否则访客可伪造头绕过统计;
  • Postgres/Redis 仅 expose 不 ports:不对宿主机暴露,降低攻击面。

4.3 启动并核对状态

cd /opt/kutt && docker compose up -d
 Container kutt-3.2.6-redis Starting
 Container kutt-3.2.6-postgres Starting
 Container kutt-3.2.6-redis Started
 Container kutt-3.2.6-postgres Started
 Container kutt-3.2.6-postgres Waiting
 Container kutt-3.2.6-postgres Healthy
 Container kutt-3.2.6-server Starting
 Container kutt-3.2.6-server Started
docker compose ps
NAME                  IMAGE                SERVICE    STATUS                    PORTS
kutt-3.2.6-postgres   postgres:16-alpine   postgres   Up (healthy)              5432/tcp
kutt-3.2.6-redis      redis:7-alpine       redis      Up                        6379/tcp
kutt-3.2.6-server     kutt/kutt:v3.2.6     server     Up                        0.0.0.0:8153->3000/tcp

应用日志确认迁移与就绪(首启自动跑 10 个迁移):

docker compose logs server | head -12
> kutt@3.2.6 migrate
> knex migrate:latest

Batch 1 run: 10 migrations

> kutt@3.2.6 start
> node server/server.js --production

> Ready on http://localhost:3000

4.4 健康检查与首启向导

curl -s http://localhost:8153/api/v2/health; echo
curl -sI http://localhost:8153/ | grep -i '^HTTP\|^location'
OK
HTTP/1.1 302 Found
Location: /create-admin

/api/v2/health 返回纯文本 OK(适合挂监控探针)。库中无任何用户时首页 302 到 /create-admin 首启向导——这是 Kutt 3.x 特有的初始化流程:浏览器打开 http://localhost:8153 设置管理员邮箱与密码(≥8 位)即可;也可纯 API 创建:

curl -s -X POST http://localhost:8153/api/v2/auth/create-admin \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@kutt.local","password":"q1w2e3r4"}'
{"token":"eyJhbGciOiJIUzI1NiIs…(JWT,实测值已截断)"}

创建成功直接返回 JWT,账号角色即 ADMIN 且 verified=true(免邮箱验证)。该端点仅在全库无用户时可用,重复调用报 Can not create the admin user because a user already exists.。

4.5 登录与 API key 发放

curl -s -X POST http://localhost:8153/api/v2/auth/login \
  -H 'Content-Type: application/json' \
  -d '{"email":"admin@kutt.local","password":"q1w2e3r4"}'
{"token":"eyJhbGciOiJIUzI1NiIs…(JWT)"}

JWT 后续调用必须走 Cookie(-b "token=<JWT>",Authorization: Bearer 不生效,实测返回 401)。用它生成脚本集成用的 API key:

curl -s -X POST http://localhost:8153/api/v2/auth/apikey -b "token=<上一步JWT>"
{"apikey":"7sGm14IdbxoknkpnItuKQVRTMBaucEQscN7kAXsJ"}

HTTP 201,key 持久有效,后续 API 调用带 x-api-key 头即可,无需维护会话。

⚠️ 说明:普通用户注册端点 /api/v2/auth/signup 实测在 MAIL_ENABLED=false(默认)时返回 401——注册强制 SMTP 邮箱验证。单机自用只建 ADMIN 即可;要开放注册必须先配第七章的 MAIL_* 变量。

五、功能验收(端到端实测)

5.1 创建短链

curl -s -X POST http://localhost:8153/api/v2/links \
  -H "x-api-key: 7sGm14IdbxoknkpnItuKQVRTMBaucEQscN7kAXsJ" \
  -H 'Content-Type: application/json' \
  -d '{"target":"https://www.quzuo.net","description":"quzuo 首页"}'
{"id":"3a39e49f-d572-4250-98f2-f79f6f3619c8","address":"6ZErgk","description":"quzuo 首页","banned":false,"password":false,"expire_in":null,"target":"https://www.quzuo.net","visit_count":0,"created_at":"2026-11-01T16:36:16.676Z","link":"https://localhost:8153/6ZErgk"}

进阶参数(自定义 slug + 密码保护 + 有效期):

curl -s -X POST http://localhost:8153/api/v2/links \
  -H "x-api-key: <APIKEY>" -H 'Content-Type: application/json' \
  -d '{"target":"https://github.com/thedevs-network/kutt","customurl":"kutt-proj","password":"q1w2e3r4","expire_in":"30d"}'
{"address":"kutt-proj","password":true,"expire_in":"2026-12-01T16:36:36.880Z","target":"https://github.com/thedevs-network/kutt","visit_count":0}

expire_in 接受 ms 库时长字符串(30d、12h、1w),存库转为绝对时间;过期链接由应用内置 cron 每 30 秒自动删除。

5.2 跳转与统计

curl -sI http://localhost:8153/6ZErgk | grep -iE '^HTTP|^location'
HTTP/1.1 302 Found
Location: https://www.quzuo.net

用浏览器(或带浏览器 UA)访问数次后查统计:

curl -s "http://localhost:8153/api/v2/links/3a39e49f-d572-4250-98f2-f79f6f3619c8/stats" \
  -H "x-api-key: <APIKEY>" | python3 -c "import json,sys; d=json.load(sys.stdin); ld=d['lastDay']; print('total:', ld['total']); print('browser:', [b for b in ld['stats']['browser'] if b['value']>0]); print('os:', [o for o in ld['stats']['os'] if o['value']>0]); print('referrer:', ld['stats']['referrer'])"
total: 3
browser: [{'name': 'chrome', 'value': 3}]
os: [{'name': 'linux', 'value': 3}]
referrer: [{'name': 'google[dot]com', 'value': 3}]

⚠️ 注意两个实测统计特性:① 访问事件经 Redis Bull 队列异步落库,且 stats 接口有 60 秒缓存,计数不会实时出现;② Kutt 用 isbot 过滤爬虫/命令行流量——curl 默认 UA 的访问完全不计数(实测 5 次访问后 visit_count 仍为 0,浏览器 UA 3 次访问后计数 3)。

5.3 密码保护链接解锁

带密码短链直接访问返回密码输入页(HTTP 200),API 解锁:

curl -s -X POST http://localhost:8153/api/v2/links/<链接UUID>/protected \
  -H "x-api-key: <APIKEY>" -H 'Content-Type: application/json' \
  -d '{"password":"q1w2e3r4"}'
{"target":"https://github.com/thedevs-network/kutt"}

六、数据持久化说明

本文栈的三处 bind mount 即全部状态,备份这三处即可整机迁移:

宿主机路径容器路径内容验证方式
/opt/kutt/data/pg/var/lib/postgresql/dataPostgres 全库(8 张表)重启后 SELECT count(*) FROM links 不变
/opt/kutt/data/redis/dataRedis AOF/RDB(缓存+队列)丢了也不损数据(统计缓存可重建)
/opt/kutt/custom/kutt/custom自定义 logo/CSS 静态资源镜像升级不覆盖

持久化实测:docker compose restart server 后重新登录并查询链接列表,total: 4 与重启前一致;Postgres 侧 \dt 可见 users/links/visits/domains/hosts/ips/knex_migrations/knex_migrations_lock 8 张表。

若选 SQLite 形态(官方默认 compose),数据落在容器内 /kutt/db/data 或 DB_FILENAME 指定路径(实测最小单容器形态首启生成 106KB 的 /kutt/db/data 文件),把该路径 bind mount 出来即可。

七、环境变量表

Kutt 无配置文件,全部经环境变量注入(源码 server/env.js 用 envalid 校验;官方样例 .example.env)。核心变量与实测默认值:

变量默认值说明
PORT3000应用监听端口
SITE_NAMEKutt站点显示名
DEFAULT_DOMAIN—对外域名(短链前缀与防自环校验)
JWT_SECRET生产必填JWT 签名密钥,缺失即启动失败(见常见问题)
DB_CLIENTbetter-sqlite3驱动:pg / mysql2 / better-sqlite3
DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASSWORDlocalhost/5432/kutt/postgres/—关系库连接(SQLite 时改用 DB_FILENAME)
LINK_LENGTH6随机短码长度
LINK_CUSTOM_ALPHABET去混淆字母表自定义 slug 允许字符(默认剔除 oO0iIl1j)
TRUST_PROXYtrue反代取真实 IP;直连应设 false
REDIS_ENABLED/HOST/PORT/PASSWORD/DBfalse/127.0.0.1/6379/—/0启用后承担缓存+Bull 队列+限流
DISALLOW_REGISTRATIONtrue默认禁自助注册(放开还需 SMTP)
DISALLOW_ANONYMOUS_LINKStrue默认禁匿名建链(本文演示设 false)
DISALLOW_LOGIN_FORMfalse配合 OIDC 时可关表单登录
CUSTOM_DOMAIN_USE_HTTPSfalse自定义域名短链前缀协议
MAIL_ENABLED/HOST/PORT/SECURE/USER/PASSWORD/FROMfalse/—/587/true/—/—/—验证邮件、找回密码、举报通知
ENABLE_RATE_LIMITfalseAPI 限流(依赖 Redis)
REPORT_EMAIL / CONTACT_EMAIL空举报收件箱 / 页面展示的联系邮箱
OIDC_ENABLED/ISSUER/CLIENT_ID/CLIENT_SECRET/SCOPE/EMAIL_CLAIM/BUTTON_TEXTfalseOIDC 单点登录
SERVER_IP_ADDRESS / SERVER_CNAME_ADDRESS空设置页展示的自定义域名解析指引

八、配置文件挂载

Kutt 3.x 用 custom 目录替代传统配置文件挂载,官方 compose 各变体统一挂 - custom:/kutt/custom。目录内约定(源码 server.js 实测核对):

  • custom/images/ → 经 /images 路径对外提供(替换 favicon/logo);
  • custom/css/ → 经 /css 路径提供自定义样式。
mkdir -p /opt/kutt/custom/images /opt/kutt/custom/css
cp my-logo.png /opt/kutt/custom/images/
curl -sI http://localhost:8153/images/my-logo.png | head -1

未挂载 custom 时目录内容随镜像走,升级镜像不会丢宿主机文件,但容器内自定义文件会随重建消失——生产务必按本文挂 bind mount。

九、等价 docker run

不用 compose 的单服务形态(SQLite,最小可跑):

docker run -d --name kutt-server -p 8153:3000 \
  -e JWT_SECRET="$(openssl rand -hex 32)" \
  -e DEFAULT_DOMAIN=localhost:8153 \
  -e SITE_NAME=Kutt \
  -e DISALLOW_ANONYMOUS_LINKS=false \
  -v /opt/kutt/sqlite:/kutt/db \
  kutt/kutt:v3.2.6
Batch 1 run: 10 migrations
> Ready on http://localhost:3000

(实测该形态健康检查 OK、首启向导与迁移行为与 compose 形态完全一致,SQLite 文件生成于挂载点内。)

十、常用运维命令

docker compose ps                     # 三服务状态
docker compose logs -f server         # 跟踪应用日志
docker compose restart server         # 仅重启应用
docker compose down                   # 停止栈(数据在 bind mount 中不丢)
docker exec -it kutt-3.2.6-postgres psql -U postgres -d kutt -c 'SELECT count(*) FROM links;'   # 直查库
docker exec kutt-3.2.6-redis redis-cli keys 'l:*'   # 查看短链缓存

日常巡检、备份恢复与版本升级的完整流程见 《Kutt 3.2.6 日常运维与升级指南》。

十一、常见问题

  1. 容器反复重启,日志报 Missing environment variables: JWT_SECRET:生产模式(NODE_ENV=production,镜像默认)下 JWT_SECRET 为必填,实测缺失时 envalid 打印 Exiting with error code 1 退出。在 .env 生成随机串后 docker compose up -d 重建即可。
  2. API 带 Authorization: Bearer <JWT> 返回 401:Kutt 3.x 的 JWT 只从 Cookie token 提取,改用 -b "token=<JWT>";脚本集成推荐直接生成 API key 走 x-api-key 头。
  3. POST /api/v2/links 匿名调用返回 401:默认 DISALLOW_ANONYMOUS_LINKS=true,放开该变量或带认证调用。
  4. 注册页提交后报 Unauthorized:自助注册强制 SMTP 验证,先配 MAIL_ENABLED=true 与 MAIL_* 全套变量。
  5. 统计数字不动:确认访问带浏览器 UA(isbot 过滤命令行流量),且等待至少 60 秒缓存窗口;Redis 关闭时统计同步写库、无队列,但 stats 缓存行为不变。

十二、参考链接

Kutt 3.2.6 Docker Compose 部署指南 - Quzuo