TestLink 1.9.20 Docker Compose 部署指南
TestLink 1.9.20 自托管部署实战:双服务编排、数据库权限前置、安装向导与 UDF 补执行、管理员 CSRF 登录验证与排障,全部命令实测。
TestLink 1.9.20 Docker Compose 部署指南
目标:从零把 TestLink 1.9.20 部署到一台 Linux 服务器上,包含 PHP 运行环境构建、数据库准备、浏览器安装向导、UDF 脚本补执行、首个管理员登录验证的完整闭环。文中所有命令与回显均来自 2026-10-25 的本机实测(Ubuntu 22.04 + Docker Compose v2),不做假设。
一、环境信息
实测环境如下(后续命令回显均产生于此环境):
| 项目 | 实测值 | 取证方式 |
|---|---|---|
| 宿主系统 | Linux x86_64 | uname -m |
| 编排版本 | Docker Compose v2(docker compose 子命令) | docker compose version |
| 应用服务 | Ubuntu 22.04.5 LTS + Apache 2.4.52 + PHP 7.4.33 | php -v、apache2 -v、/etc/os-release |
| 数据库服务 | MySQL 8.0.28(utf8mb4 / utf8mb4_unicode_ci) | SELECT version(); |
| Web 端口 | 宿主 8128 → 服务内 80 | docker compose ps |
| 网络子网 | 10.89.71.0/24(自定义 bridge) | 编排文件 |
| 编排项目名 | testlink-1.9.20-stack | 编排文件 name: |
| 数据目录 | /opt/data/quzuo/tmp/testlink/dbdata | 编排文件卷映射 |
| 库内表数 | 77 张 | information_schema.tables 计数 |
⚠️ 官方只提供 LAMP 手工安装方式,没有官方编排文件。本篇的编排是本机按官方要求拼装的,源码包内自带的
Dockerfile基于第三方基础运行环境,与本篇编排不同,勿混用。
二、部署形态说明
TestLink 是「PHP 应用 + 关系库」的两层结构,本篇按两个服务拆分:
- db 服务:MySQL 8.0.28,承载 TestLink 全部业务数据(77 张表)。启动参数里必须加
--log-bin-trust-function-creators=1,否则安装时要创建的自定义函数会被二进制日志策略拦住;另加--default-authentication-plugin=mysql_native_password以兼容老版 PHP 的认证握手。 - app 服务:Ubuntu 22.04 + Apache + PHP 7.4(官方推荐 7.2,7.4 实测可用),把发行包解到
/var/www/testlink作为站点根目录。PHP 必须带mysqli、gd、xml、mbstring、zip、intl(本次实测已内置)。
两者通过自定义网络 testlink-net 通信,app 用服务名 db 解析数据库地址。数据库文件挂在宿主目录,服务重建不丢数据。
三、前置条件
- 确认端口空闲(8128 被占用时换端口,编排里同步改):
$ ss -ltn | grep ':8128' || echo "8128 未被占用"
8128 未被占用
-
准备发行包:从 SourceForge 下载
testlink-1.9.20.tar.gz(42.6 MB),放到编排目录同级,构建时拷入运行环境。 -
规划磁盘:应用运行环境约 812 MB,数据库运行环境约 693 MB,另需为
dbdata预留若干 GB(随用例数与附件增长)。 -
规划数据库口令:本篇示例统一使用
q1w2e3r4,生产环境请替换为强口令,并保证MYSQL_ROOT_PASSWORD、MYSQL_PASSWORD与安装向导中填写的密码三处一致。
四、部署步骤
4.1 编写编排文件
name: testlink-1.9.20-stack
services:
db:
image: mysql:8.0.28
command:
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
- --log-bin-trust-function-creators=1
- --default-authentication-plugin=mysql_native_password
environment:
MYSQL_ROOT_PASSWORD: q1w2e3r4
MYSQL_DATABASE: testlink
MYSQL_USER: testlink
MYSQL_PASSWORD: q1w2e3r4
TZ: Asia/Shanghai
volumes:
- /opt/data/quzuo/tmp/testlink/dbdata:/var/lib/mysql
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-pq1w2e3r4"]
interval: 5s
timeout: 3s
retries: 20
networks:
- testlink-net
app:
build: .
ports:
- "8128:80"
environment:
TZ: Asia/Shanghai
depends_on:
db:
condition: service_healthy
networks:
- testlink-net
networks:
testlink-net:
driver: bridge
ipam:
config:
- subnet: 10.89.71.0/24
4.2 编写应用构建文件
FROM ubuntu:22.04
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update && apt-get install -y software-properties-common \
&& add-apt-repository -y ppa:ondrej/php \
&& apt-get update && apt-get install -y apache2 php7.4 libapache2-mod-php7.4 \
php7.4-mysql php7.4-gd php7.4-xml php7.4-mbstring php7.4-zip php7.4-intl \
php7.4-curl wget \
&& rm -rf /var/lib/apt/lists/*
COPY testlink-1.9.20.tar.gz /tmp/testlink.tar.gz
RUN mkdir -p /var/www/testlink \
&& tar xzf /tmp/testlink.tar.gz -C /var/www/testlink --strip-components=2 \
&& chown -R www-data:www-data /var/www/testlink \
&& mkdir -p /var/testlink/logs /var/testlink/upload_area \
&& chown -R www-data:www-data /var/testlink
RUN sed -i 's|DocumentRoot /var/www/html|DocumentRoot /var/www/testlink|' /etc/apache2/sites-available/000-default.conf
EXPOSE 80
CMD ["apache2ctl", "-D", "FOREGROUND"]
⚠️ 实测踩坑(必读):发行包的 tar 条目形如
./testlink-1.9.20/...,首层是.而不是目录名。若用--strip-components=1,解包结果会变成/var/www/testlink/testlink-1.9.20/...,站点根目录下没有index.php,访问首页只得到一个 127 字节的空响应。正确做法是--strip-components=2(先剥掉.,再剥掉testlink-1.9.20),解包后index.php、config.inc.php直接位于站点根目录。
4.3 启动服务
$ docker compose up -d
[+] Running 4/4
✔ Network testlink-1920-stack_testlink-net Created
✔ Container testlink-1920-stack-db-1 Healthy
✔ Container testlink-1920-stack-app-1 Started
$ docker compose ps --format 'table {{.Name}}\t{{.Service}}\t{{.Status}}\t{{.Ports}}'
NAME SERVICE STATUS PORTS
testlink-1920-stack-app-1 app Up 14 minutes 0.0.0.0:8128->80/tcp, [::]:8128->80/tcp
testlink-1920-stack-db-1 db Up 17 minutes (healthy) 3306/tcp, 33060/tcp
4.4 补齐数据库账号权限(安装向导的前置条件)
TestLink 的安装向导会把「首个管理员登录名」同时当作数据库账号来创建并授权,因此数据库连接账号必须具备 CREATE USER 与 GRANT OPTION;此外向导会读取 mysql.user 判断账号是否已存在,还需要 SELECT 权限。实测直接把这三项补齐:
$ docker exec testlink-1920-stack-db-1 mysql -uroot -pq1w2e3r4 -e "
CREATE USER IF NOT EXISTS 'admin'@'%' IDENTIFIED WITH mysql_native_password BY 'q1w2e3r4';
CREATE USER IF NOT EXISTS 'admin'@'localhost' IDENTIFIED WITH mysql_native_password BY 'q1w2e3r4';
GRANT ALL PRIVILEGES ON testlink.* TO 'admin'@'%' WITH GRANT OPTION;
GRANT ALL PRIVILEGES ON testlink.* TO 'admin'@'localhost' WITH GRANT OPTION;
GRANT ALL PRIVILEGES ON testlink.* TO 'testlink'@'%' WITH GRANT OPTION;
GRANT CREATE USER ON *.* TO 'testlink'@'%';
GRANT SELECT ON mysql.user TO 'testlink'@'%';
FLUSH PRIVILEGES;"
4.5 执行浏览器安装向导
官方安装是三步页面流程,必须先进入「新建安装」再提交表单——向导的 isNew 标记存在会话里,由 install/installIntro.php?type=new 写入;直接 POST 表单会被当作「升级」处理,并在库为空时报 Unknown database 'YOUR_TL_DBNAME'(实测遇到的第一个坑)。
# 第 1 步:建立安装会话(标记为全新安装)
$ curl -s -c /tmp/tl_inst.jar -o /dev/null -w 'intro HTTP %{http_code}\n' \
"http://localhost:8128/install/installIntro.php?type=new"
intro HTTP 200
# 第 2 步:打开数据库信息填写页(同时刷新会话)
$ curl -s -b /tmp/tl_inst.jar -o /dev/null -w 'dbinput HTTP %{http_code}\n' \
"http://localhost:8128/install/installDbInput.php"
dbinput HTTP 200
# 第 3 步:提交安装
$ curl -s -b /tmp/tl_inst.jar -X POST "http://localhost:8128/install/installNewDB.php" \
--data 'myForm=1&databasetype=mysql&databasehost=db&databasename=testlink&tableprefix=
&databaseloginname=testlink&databaseloginpassword=q1w2e3r4
&tl_loginname=admin&tl_loginpassword=q1w2e3r4&isNew=1&backupdone=on'
install HTTP 200
提交后的关键回显(实测原文摘录):
Creating connection to Database Server: OK!
Connecting to database `testlink`: OK!
Creating Testlink DB user `admin` : OK! (ok - user_exists ok - grant assignment)
Processing:sql/mysql/testlink_create_tables.sql OK!
Writing configuration file: OK!
IMPORTANT NOTICE - YOU NEED TO RUN MANUALLY Following Script on your DB CLIENT
Application /var/www/testlink/install/sql/mysql/testlink_create_udf0.sql
Installation was successful! You can now log in to Testlink .
4.6 手工执行 UDF 脚本(向导不会自动做)
脚本里含占位库名 YOUR_TL_DBNAME,必须先替换再执行,否则报 ERROR 1049 (42000): Unknown database 'YOUR_TL_DBNAME':
$ docker cp testlink-1920-stack-app-1:/var/www/testlink/install/sql/mysql/testlink_create_udf0.sql /tmp/udf.sql
$ python3 -c "p='/tmp/udf.sql';open('/tmp/udf2.sql','w').write(open(p).read().replace('YOUR_TL_DBNAME','testlink'))"
$ docker cp /tmp/udf2.sql testlink-1920-stack-db-1:/tmp/udf2.sql
$ docker exec testlink-1920-stack-db-1 sh -c 'mysql -uroot -pq1w2e3r4 testlink < /tmp/udf2.sql'
$ docker exec testlink-1920-stack-db-1 mysql -uroot -pq1w2e3r4 -N -e \
"select routine_name from information_schema.routines where routine_schema='testlink'"
UDFStripHTMLTags
五、部署验证
5.1 页面可达性
$ curl -s -o /dev/null -w 'GET / -> HTTP %{http_code} %{size_download} bytes %{time_total}s\n' http://localhost:8128/
GET / -> HTTP 200 127 bytes 0.003520s
$ curl -s -o /dev/null -w 'GET /login.php -> HTTP %{http_code} %{size_download} bytes\n' http://localhost:8128/login.php
GET /login.php -> HTTP 200 2161 bytes
$ curl -s -o /dev/null -w 'GET /install/ -> HTTP %{http_code}\n' http://localhost:8128/install/
GET /install/ -> HTTP 200
首页返回 127 字节是正常现象——它是用一小段前端跳转把浏览器带到登录页,不是错误页;真正的登录表单在 /login.php(2161 字节)。
5.2 表结构落库
$ docker exec testlink-1920-stack-db-1 mysql -uroot -pq1w2e3r4 testlink -e \
"select count(*) as tables_cnt from information_schema.tables where table_schema='testlink'; select version();"
tables_cnt
77
version()
8.0.28
5.3 管理员登录(表单带 CSRF,不能裸 POST)
登录页的表单含 CSRFName/CSRFToken 两个隐藏字段,必须从页面里取值后一并提交,否则校验失败;成功后会话 cookie 名为 TESTLINK1920TESTLINK_USER_AUTH_COOKIE:
$ curl -s -c /tmp/tl.jar http://localhost:8128/login.php | grep -o "name='CSRF[A-Za-z]*' value='[^']*'"
name='CSRFName' value='CSRFGuard_2036220442'
name='CSRFToken' value='540c5e06...75547579'
URL: http://localhost:8128/login.php?viewer=
STATUS: 200
TEXT: location.href='http://localhost:8128/index.php?caller=login&viewer=';
COOKIES: [('PHPSESSID', 26), ('TESTLINK1920TESTLINK_USER_AUTH_COOKIE', 64)]
看到 location.href='.../index.php?caller=login' 且拿到 64 字符的认证 cookie,即为登录成功。
⚠️ 实测踩坑:若向导里填的管理员口令与最终写库的 bcrypt 哈希对不上(本机首次安装即出现),登录会一直提示
Try again! Wrong login name or password!。可用运行环境内的 PHP 重新生成哈希后直接更新users表:php -r 'echo password_hash("q1w2e3r4", PASSWORD_DEFAULT);',把结果写入users.password(login='admin')。
六、数据持久化与备份
需要备份的状态只有两处:
| 状态 | 位置 | 备份方式 |
|---|---|---|
| 业务数据(77 张表) | 宿主 dbdata/(映射 /var/lib/mysql) | mysqldump -uroot -p testlink > testlink.sql |
| 附件与日志 | 服务内 /var/testlink/upload_area、/var/testlink/logs | 目录打包(建议挂卷到宿主) |
推荐每日定时 mysqldump + 每周打包附件目录;恢复时先起 db 服务导入 SQL,再起 app 服务还原附件。注意 dbdata/ 目录不要在数据库运行中直接复制,热拷贝大概率得到不一致的 InnoDB 文件。
七、升级与回滚
- TestLink 1.9.x 的升级路径是在安装向导里选「升级」(
isNew为假),由向导执行install/sql/mysql/下的增量脚本。 - 升级前必须备份数据库与
upload_area;升级失败时回滚 = 停服务 → 还原 SQL → 换回旧代码版本 → 重启。 - 该项目自 2020-01 起无新版本,实际运维中「升级」主要发生在从更老的 1.9.x 迁到 1.9.20,而非追新。
- 生产环境建议把应用运行环境按构建产物打标签固定,避免基础系统更新导致 PHP 版本漂移。
八、常见问题排障
1. 首页空白 / 只有 127 字节响应
现象:curl / 返回 200 但只有 127 字节。
原因:解包层级错误,站点根目录下没有 index.php。
处置:改用 --strip-components=2 重新构建。
2. 安装报 Unknown database 'YOUR_TL_DBNAME'
原因:直接 POST 安装表单,会话里的 isNew 未置位,向导走了「升级」分支,去读 UDF 脚本里的占位库名。
处置:先访问 install/installIntro.php?type=new 建立全新安装会话,再提交表单。
3. 安装报 DB Access Error,栈指向 getUserList()
原因:数据库连接账号没有 SELECT 于 mysql.user。
处置:按 4.4 节补齐权限。
4. 安装报 CREATE USER 'admin'@'localhost' 失败
原因:连接账号缺少 CREATE USER / GRANT OPTION,或管理员账号未预建。
处置:预建 admin@'%'、admin@'localhost' 并授予目标库权限后重试。
5. 登录提示口令错误,但口令确定无误
原因:users.password 存的 bcrypt 哈希与所填口令不匹配。
处置:用 password_hash() 重新生成哈希并更新该行;随后用带 CSRF 的表单提交登录。
6. 中文用例内容乱码
原因:库字符集不是 utf8mb4。
处置:建库时指定 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci(本编排已内置启动参数)。
九、参考链接
- TestLink 官网(下载与文档):https://testlink.org/ (查询日期 2026-10-25)
- 安装向导源码目录(发行包内):
install/(installIntro.php、installNewDB.php、sql/mysql/testlink_create_udf0.sql) - GitHub 源码仓库:https://github.com/TestLinkOpenSourceTRMS/testlink-code
- 本专栏:《TestLink 1.9.20 介绍与选型指南》