Kutt 3.2.6 Docker Compose 部署指南
Ubuntu 24.04 服务器上用 Docker Compose 部署 Kutt 3.2.6 短链服务全实录:官方四变体与预构建镜像选型、Postgres16+Redis7 三服务编排、首启建管与 API 全功能实测、数据持久化验证、环境变量表与等价 docker run,全部命令回显实测。
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,镜像 IDfa3d24a89b04,本地解压 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.yml | SQLite(默认) | 无 | 官方默认,单服务最简形态 |
docker-compose.sqlite-redis.yml | SQLite | 有 | 加缓存与访问队列 |
docker-compose.postgres.yml | PostgreSQL | 有 | 生产推荐(本文采用其编排) |
docker-compose.mariadb.yml | MariaDB 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/data | Postgres 全库(8 张表) | 重启后 SELECT count(*) FROM links 不变 |
/opt/kutt/data/redis | /data | Redis 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)。核心变量与实测默认值:
| 变量 | 默认值 | 说明 |
|---|---|---|
PORT | 3000 | 应用监听端口 |
SITE_NAME | Kutt | 站点显示名 |
DEFAULT_DOMAIN | — | 对外域名(短链前缀与防自环校验) |
JWT_SECRET | 生产必填 | JWT 签名密钥,缺失即启动失败(见常见问题) |
DB_CLIENT | better-sqlite3 | 驱动:pg / mysql2 / better-sqlite3 |
DB_HOST/DB_PORT/DB_NAME/DB_USER/DB_PASSWORD | localhost/5432/kutt/postgres/— | 关系库连接(SQLite 时改用 DB_FILENAME) |
LINK_LENGTH | 6 | 随机短码长度 |
LINK_CUSTOM_ALPHABET | 去混淆字母表 | 自定义 slug 允许字符(默认剔除 oO0iIl1j) |
TRUST_PROXY | true | 反代取真实 IP;直连应设 false |
REDIS_ENABLED/HOST/PORT/PASSWORD/DB | false/127.0.0.1/6379/—/0 | 启用后承担缓存+Bull 队列+限流 |
DISALLOW_REGISTRATION | true | 默认禁自助注册(放开还需 SMTP) |
DISALLOW_ANONYMOUS_LINKS | true | 默认禁匿名建链(本文演示设 false) |
DISALLOW_LOGIN_FORM | false | 配合 OIDC 时可关表单登录 |
CUSTOM_DOMAIN_USE_HTTPS | false | 自定义域名短链前缀协议 |
MAIL_ENABLED/HOST/PORT/SECURE/USER/PASSWORD/FROM | false/—/587/true/—/—/— | 验证邮件、找回密码、举报通知 |
ENABLE_RATE_LIMIT | false | API 限流(依赖 Redis) |
REPORT_EMAIL / CONTACT_EMAIL | 空 | 举报收件箱 / 页面展示的联系邮箱 |
OIDC_ENABLED/ISSUER/CLIENT_ID/CLIENT_SECRET/SCOPE/EMAIL_CLAIM/BUTTON_TEXT | false | OIDC 单点登录 |
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 日常运维与升级指南》。
十一、常见问题
- 容器反复重启,日志报
Missing environment variables: JWT_SECRET:生产模式(NODE_ENV=production,镜像默认)下JWT_SECRET为必填,实测缺失时 envalid 打印Exiting with error code 1退出。在.env生成随机串后docker compose up -d重建即可。 - API 带
Authorization: Bearer <JWT>返回 401:Kutt 3.x 的 JWT 只从 Cookietoken提取,改用-b "token=<JWT>";脚本集成推荐直接生成 API key 走x-api-key头。 POST /api/v2/links匿名调用返回 401:默认DISALLOW_ANONYMOUS_LINKS=true,放开该变量或带认证调用。- 注册页提交后报 Unauthorized:自助注册强制 SMTP 验证,先配
MAIL_ENABLED=true与MAIL_*全套变量。 - 统计数字不动:确认访问带浏览器 UA(isbot 过滤命令行流量),且等待至少 60 秒缓存窗口;Redis 关闭时统计同步写库、无队列,但 stats 缓存行为不变。