TestLink 1.9.20 Docker Compose 部署指南

TestLink 1.9.20 自托管部署实战:双服务编排、数据库权限前置、安装向导与 UDF 补执行、管理员 CSRF 登录验证与排障,全部命令实测。

TestLinkTestLink测试管理自托管Docker Compose2026/10/25

TestLink 1.9.20 Docker Compose 部署指南

目标:从零把 TestLink 1.9.20 部署到一台 Linux 服务器上,包含 PHP 运行环境构建、数据库准备、浏览器安装向导、UDF 脚本补执行、首个管理员登录验证的完整闭环。文中所有命令与回显均来自 2026-10-25 的本机实测(Ubuntu 22.04 + Docker Compose v2),不做假设。

一、环境信息

实测环境如下(后续命令回显均产生于此环境):

项目实测值取证方式
宿主系统Linux x86_64uname -m
编排版本Docker Compose v2(docker compose 子命令)docker compose version
应用服务Ubuntu 22.04.5 LTS + Apache 2.4.52 + PHP 7.4.33php -v、apache2 -v、/etc/os-release
数据库服务MySQL 8.0.28(utf8mb4 / utf8mb4_unicode_ci)SELECT version();
Web 端口宿主 8128 → 服务内 80docker 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 解析数据库地址。数据库文件挂在宿主目录,服务重建不丢数据。

三、前置条件

  1. 确认端口空闲(8128 被占用时换端口,编排里同步改):
$ ss -ltn | grep ':8128' || echo "8128 未被占用"
8128 未被占用
  1. 准备发行包:从 SourceForge 下载 testlink-1.9.20.tar.gz(42.6 MB),放到编排目录同级,构建时拷入运行环境。

  2. 规划磁盘:应用运行环境约 812 MB,数据库运行环境约 693 MB,另需为 dbdata 预留若干 GB(随用例数与附件增长)。

  3. 规划数据库口令:本篇示例统一使用 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(本编排已内置启动参数)。

九、参考链接