Meetable Docker Compose 部署指南

Meetable(latest=main@7d2d22ab,镜像 2026-10-16 构建)Docker Compose 部署实测指南:官方四容器栈逐段解析、.env 四个必填项、entrypoint 自动迁移行为、管理员创建(passkey/tinker 双路径)、事件页/iCal/JSON/短链闭环验收、scheduler 缺口补位与升级备份,全部命令逐字回显(实测端口 8158,2026-11-02)

MeetableMeetable自托管DockerLaravelMariaDBRedis2026/11/02

Meetable Docker Compose 部署指南

目标:在单机 Docker 环境把 Meetable 从零部署到功能闭环验收。本文全部配置、命令与输出均为 2026-11-02 实测逐字记录(取证全文见 /opt/data/quzuo/tmp/meetable/evidence.md)。

甄别注记:① 批次计划文档称 Meetable 为 Rails 应用、README 提 launchd——实测均不成立,Meetable 是 Laravel/PHP 应用,README 全文无 launchd 字样,官方推荐安装方式即本文的 Docker Compose(README "Setup" 首选)。② 本环境浏览器自动化工具不可用,页面验收降级为 HTTP 状态码 + <title> + API 闭环(§7)。③ 计划端口 8157 实测已被占用,改用 8158。

一、实测环境

项值
宿主 OSUbuntu 24.04.5 LTS(内核 7.0.0-38-generic)
Docker / Compose29.9.2 / v5.6.1
应用镜像ghcr.io/aaronpk/meetable:latest(created 2026-10-16T22:21:49Z,即 main@7d2d22ab7f85)
Web 端口127.0.0.1:8158(官方默认 127.0.0.1:8080,本实测仅改宿主端口)
部署目录/opt/data/quzuo/tmp/meetable/

二、获取官方编排文件

README 指定的两条命令(实测逐字执行):

curl -O https://raw.githubusercontent.com/aaronpk/Meetable/main/docker-compose.yml
curl -o .env https://raw.githubusercontent.com/aaronpk/Meetable/main/.env.example

2.1 官方 docker-compose.yml 逐段解析(实测原文)

公共锚点 x-app-config——app 与 worker 共用同一镜像与配置:

x-app-config: &app-config
  image: ghcr.io/aaronpk/meetable:latest
  restart: unless-stopped
  env_file:
    - .env
  environment:
    # Overriding these to force the app to use the docker compose services instead of what might be defined in .env
    - DB_HOST=db
    - REDIS_HOST=redis
    - CACHE_DRIVER=redis
    - QUEUE_CONNECTION=redis
    - SESSION_DRIVER=redis

⚠️ 关键设计:environment 列表强制覆盖 .env 中的同名五项——你在 .env 里把 CACHE_DRIVER 改成 file 也不会生效,compose 栈内应用永远走 db 容器与 redis 容器。实测 php artisan about 显示 Cache=redis、Database=mysql,与 .env 无关。

四个服务:

services:
  app:                      # Web 前端(Apache+PHP 同容器)
    <<: *app-config
    ports: ["127.0.0.1:8080:80/tcp"]   # 本实测改 8158;默认只绑回环,公网需反代
    depends_on: [db, redis]
    volumes: [meetable_storage:/var/www/html/storage/app]   # 上传图片持久化
  worker:                   # 队列消费者(头像下载、webmention 抓取等异步任务)
    <<: *app-config
    command: ["php", "artisan", "queue:work"]
    volumes: [meetable_storage:/var/www/html/storage/app]
  db:                       # 业务数据库
    image: mariadb:11.8     # LTS 版;MARIADB_AUTO_UPGRADE=1 支持旧数据目录原地升级
    environment:
      MYSQL_DATABASE: ${DB_DATABASE:-laravel}
      MYSQL_USER: ${DB_USERNAME:-meetable}
      MYSQL_PASSWORD: ${DB_PASSWORD}
      MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD:-root}
    volumes: [meetable_db:/var/lib/mysql]
  redis:
    image: redis:8-alpine
    volumes: [meetable_redis:/data]
volumes: {meetable_storage: , meetable_db: , meetable_redis: }

端口映射修改(本实测):"127.0.0.1:8080:80/tcp" → "127.0.0.1:8158:80/tcp"。

三、.env 必填四项

README 明示最小配置集,实测有效:

APP_KEY=<openssl rand -hex 16 的输出>      # 32 字符随机串,加密会话/签名 URL 用
APP_URL=http://127.0.0.1:8158             # 无结尾斜杠;影响 permalink/ICS 内 URL
DB_PASSWORD=Me3tBl3-S3cret-Pw             # mariadb 业务账户密码(无默认,必须设)
DB_ROOT_PASSWORD=Ro0t-S3cret-Pw           # mariadb root 密码(compose 变量插值用)

本实测完整 .env(其余为分类默认值,全量参数解析见 C1):

APP_NAME=Meetable
APP_ENV=production
APP_KEY=0645ee0b0e5d68ae9d97d589bb1b0440
APP_URL=http://127.0.0.1:8158
DB_CONNECTION=mysql
DB_PORT=3306
DB_DATABASE=meetable
DB_USERNAME=meetable
DB_PASSWORD=…
DB_ROOT_PASSWORD=…
CACHE_DRIVER=redis
QUEUE_CONNECTION=redis
SESSION_DRIVER=redis
REDIS_HOST=redis
FILESYSTEM_DRIVER=local
AUTH_METHOD=session
AUTH_SHOW_LOGIN=true
AUTH_SHOW_LOGOUT=true
ALLOW_MANAGE_EVENTS=admins
ALLOW_MANAGE_SITE=admins

四、启动与首次初始化(实测日志)

$ docker compose up -d
 Network meetable_default Created
 Container meetable-redis-1 Started
 Container meetable-db-1 Started
 Container meetable-app-1 Started
 Container meetable-worker-1 Started

$ docker compose ps
NAME                IMAGE                             STATUS                  PORTS
meetable-app-1      ghcr.io/aaronpk/meetable:latest   Up                      127.0.0.1:8158->80/tcp
meetable-db-1       mariadb:11.8                      Up                      3306/tcp
meetable-redis-1    redis:8-alpine                    Up                      6379/tcp
meetable-worker-1   ghcr.io/aaronpk/meetable:latest   Up                      80/tcp

app 容器 entrypoint(镜像内 /usr/local/bin/entrypoint.sh)实测行为:mysqladmin ping 轮询等库就绪 → 若首参为 apache2-foreground 则 php artisan storage:link + php artisan migrate --force → exec 原命令拉起 Apache。关键日志:

Waiting for MySQL database to be ready...
Database is ready!
Creating storage symlink...
Running database migrations...
  2026_10_15_000000_response_photos_removed ...... 2.14ms DONE   (60+ 条全部 DONE)
Starting original command: apache2-foreground
AH00163: Apache/2.4.68 (Debian) PHP/8.2.34 configured -- resuming normal operations

即:首启无需手动 migrate,compose 起来即完成建表(60+ 迁移,users/events/responses/passkeys/discord_notifications 等)。

五、创建管理员

5.1 官方路径(浏览器,passkey)

AUTH_METHOD=session 下访问 /login,实测页面为 "Create Admin User" 表单(name/email + WebAuthn passkey 注册,POST /auth/create-user)。首个注册者即管理员;此后 Web 界面不再允许创建用户。⚠️ passkey 注册依赖真实浏览器的 WebAuthn(https 或 localhost 安全上下文),headless 自动化不可行。

5.2 无浏览器降级路径(tinker,本实测采用)

模型命名空间为旧式 App\User/App\Event(不是 App\Models\User,实测 Class "App\Models\User" not found);create() 受 mass-assignment 限制只写入时间戳(报 Field 'identifier' doesn't have a default value),需 forceCreate:

$ docker exec meetable-app-1 php artisan tinker --execute="
\$u = App\User::forceCreate(['name'=>'Admin','email'=>'admin@example.org',
  'identifier'=>'http://example.org/admin','is_admin'=>1,
  'default_timezone'=>'Asia/Shanghai']);
echo 'user_id='.\$u->id;"
user_id=1

identifier 为用户唯一 ID(URL 形态)。之后可用 §5.1 的 passkey 流程绑定登录,或按需切换 GitHub/OIDC 认证后由白名单用户登录自动成为管理员(见 C1)。

5.3 建演示事件(同样注意 key 长度)

docker exec meetable-app-1 php artisan tinker --execute="
\$e = App\Event::forceCreate(['key'=>'quzuo1demo01','slug'=>'quzuo-demo-event',
 'name'=>'Quzuo Demo Event','status'=>'scheduled','start_date'=>'2026-11-10',
 'end_date'=>'2026-11-10','start_time'=>'19:00:00','end_time'=>'20:00:00',
 'timezone'=>'Asia/Shanghai','sort_date'=>'2026-11-10 19:00:00',
 'location_name'=>'Online','created_by'=>1,'rsvps_enabled'=>1]);
echo 'event_id='.\$e->id;"

⚠️ 实测踩坑:Event::find_from_url() 用正则 [0-9a-zA-Z]{12} 精确匹配 12 位 key;建事件时给了 6 位 quzuo1,事件页与短链一律 404,改成 12 位 quzuo1demo01 后立即 200。Web 界面创建的事件 key 由系统自动生成 12 位,手工造数据必须遵守此约束。

六、"签到/核验"流程按实甄别

Meetable 没有签到二维码(全量路由 grep qr|checkin 无匹配)。其到场登记 = 登录用户在事件页 RSVP(POST /event/{event}/rsvp,yes/if-needed/no),管理员在 /event/{id}/responses 查看与审核。本实测验证的对外闭环是 事件 permalink / 短链 / JSON / iCal 订阅 URL:

GET /2026/11/quzuo-demo-event-quzuo1demo01 → 200  <title>Quzuo Demo Event | November 10, 2026 | Meetable</title>
GET /quzuo1demo01                          → 301  → 上列 canonical URL
GET /event/quzuo1demo01.json               → 200  {"meeting_url":false,"meeting_state":"none"}
GET /ics/events.ics                        → 200  VCALENDAR;DTSTART:20261110T110000Z(19:00 CST→UTC 正确)
GET /tag/test.ics                          → 200

七、部署级验收清单(HTTP 降级验收,实测全过)

检查项命令/URL实测结果
首页curl -s http://127.0.0.1:8158/ | grep '<title>'200,Meetable | Meetable;建事件后含 Quzuo Demo Event
归档页/archive200,Past Events | Meetable
登录入口/login200,含 Create Admin User 表单
事件页/2026/11/<slug>-<12位key>200(§5.3 踩坑修复后)
iCal/ics/events.ics200,text/calendar,VEVENT 完整
队列docker exec meetable-worker-1 php artisan tinker --execute="echo DB::table('jobs')->count();"0 pending
MCP 端点POST /mcp(GET 返回 405)405(仅 POST,符合预期)

八、scheduler 缺口与补位(重要)

php artisan schedule:list 实测列出 4 个定时任务(event:notify、discord:notify、discord:sync-events 每分钟;recurring:schedule 每日 0 点),但 compose 栈内无任何进程执行 schedule:run(app 仅 apache2、worker 仅 queue:work,ps 实测确认)。纯 compose 部署下会议提醒、Discord 同步、周期事件生成不会触发。补位方案(任选其一):

# docker-compose.override.yml 追加第五容器
  scheduler:
    image: ghcr.io/aaronpk/meetable:latest
    restart: unless-stopped
    env_file: [.env]
    entrypoint: ["/bin/sh","-c","while true; do php artisan schedule:run --no-interaction; sleep 60; done"]
    depends_on: [db, redis]
    volumes: [meetable_storage:/var/www/html/storage/app]

或宿主机 crontab:* * * * * docker exec meetable-app-1 php artisan schedule:run >> /dev/null 2>&1。

九、生产化、升级与备份

  • 反代+TLS:端口只绑 127.0.0.1 是有意的(README:put a reverse proxy with TLS in front)。Nginx 直接 proxy_pass http://127.0.0.1:8158 即可(容器内已是 Apache 全栈,无需 fastcgi)。APP_URL 同步改为公网 https 域名。
  • 升级:无版本发布,升级=滚动 main。docker compose pull && docker compose up -d,entrypoint 自动 migrate --force;先 docker run --rm -v meetable_meetable_db:/var/lib/mysql alpine tar czf /backup/db-pre-$(date +%F).tar.gz -C /var/lib/mysql . 备份卷(或 mariadb 容器内 mariadb-dump)。回滚:给 compose 钉住旧 image digest(本实测 digest sha256:3232653bf985…)。
  • 备份三件套:meetable_db 卷(业务数据)、meetable_storage 卷(上传图片)、.env(含 APP_KEY,丢失则会话失效、签名链接作废)。

十、常见故障速查

现象原因处置
事件页/短链 404key 非 12 位字母数字(§5.3)修正 key 或走 Web 界面创建
tinker 建用户报 identifier 无默认值mass-assignment用 forceCreate
改了 .env 的 CACHE_DRIVER 不生效compose 锚点强制覆盖(§2.1)改 compose 的 environment
提醒/周期事件不生成栈内无 scheduler(§8)补 scheduler 容器
app 容器反复重启DB 密码与 mariadb 卷内旧密码不一致docker compose down 后清卷重来,或进 mariadb 改密
worker 报 redis 连接失败首启时序restart: unless-stopped 会自愈,无需干预

十一、关联文档

Meetable Docker Compose 部署指南 - Quzuo