Featured image of post Docker部署余哥礼簿

Docker部署余哥礼簿

记录一次从零开始在黑群晖 DSM 7.2 上用 Docker 部署 PHP + SQLite 礼金簿应用的完整过程,包含 8 个真实踩坑点:镜像加速、Alpine 源优化、权限治理、nginx 防护、二维码放行、reload 静默失败等。

字数: 2270

记录一次完整的黑群晖 NAS 部署过程,包含镜像加速、Alpine 源优化、权限治理、nginx 防护等 6 个真实踩坑点。

一、背景

手上有一套自研的「余哥礼簿」PHP 应用(PHP 8.2 + SQLite,无外部数据库依赖),准备部署到黑群晖上,供家庭局域网使用——婚丧嫁娶时手机、平板、大屏随时录入和查看。

选择 Docker 而非 Web Station 的理由:

  • 环境隔离,不污染 DSM 系统
  • 迁移方便,换 NAS 直接搬目录
  • 可以锁定 PHP 版本和扩展,避免 DSM 升级打断

部署环境:

项目值
群晖版本DSM 7.2+(Container Manager)
存储卷volume5(docker 共享文件夹所在)
项目路径/volume5/docker/giftbook/
访问端口8080
内网 IP192.168.31.82

二、架构设计

采用单容器方案——Nginx + PHP-FPM 装在同一个容器里,用 supervisord 同时管理两个进程。

text

群晖 NAS
└── /volume5/docker/giftbook/
    ├── docker-compose.yml
    ├── Dockerfile
    ├── nginx/default.conf
    ├── php/uploads.ini
    ├── data/                    ← SQLite + 二维码 + 草稿(持久化)
    │   ├── giftbook.db
    │   ├── upload_qrcode/
    │   └── draft/
    └── 所有 PHP 源码

为什么不拆成双容器(Nginx 容器 + FPM 容器): 双容器需要配置 FastCGI 网络通信 + 共享卷双重权限映射,在群晖 ACL 体系下极易出现 File not found 或 Permission denied。单容器把这些问题全部消除。

三、踩坑全记录

坑 1:群晖的 Docker 不是 systemd 服务

症状:

bash

$ systemctl restart docker
Failed to restart docker.service: Unit docker.service failed to load: No such file or directory.

原因: 群晖把 Docker 打包成 SPK 套件,不走 systemd。

正解:

bash

# DSM 7.2+ 用 ContainerManager
sudo synopkg restart ContainerManager

# 旧版用 Docker
sudo synopkg restart Docker

坑 2:镜像加速器配置位置不同于标准 Linux

症状: 改了 /etc/docker/daemon.json 加速器不生效。

原因: 群晖的 dockerd 读的是自己的配置文件,不是 /etc/docker/daemon.json。

正解: DSM 7.2+ 修改 /var/packages/ContainerManager/etc/dockerd.json:

bash

sudo tee /var/packages/ContainerManager/etc/dockerd.json > /dev/null <<'JSON'
{
  "registry-mirrors": ["https://rif02pwzp51o4duzz2.xuanyuan.run"],
  "insecure-registries": ["rif02pwzp51o4duzz2.xuanyuan.run"]
}
JSON

sudo chmod 644 /var/packages/ContainerManager/etc/dockerd.json
sudo synopkg restart ContainerManager

# 验证
docker info | grep -A 3 "Registry Mirrors"

坑 3:Alpine 的 SQLite 开发包名不是 libsqlite3-dev

症状: Dockerfile 构建时报:

text

ERROR: unable to select packages:
  libsqlite3-dev (no such package)

原因: 从 Debian 系切到 Alpine 系,包名规则变了。Debian 叫 libsqlite3-dev,Alpine 叫 sqlite-dev。

正解:

dockerfile

RUN apk add --no-cache nginx supervisor sqlite-dev \
    && docker-php-ext-install pdo pdo_sqlite \
    && docker-php-ext-enable pdo_sqlite

坑 4:Alpine 官方源慢到离谱

症状: 一次构建 apk add 阶段耗时 966 秒(16 分钟)。

原因: Alpine 官方 CDN(dl-cdn.alpinelinux.org)在国内访问极慢。

正解: Dockerfile 里换清华源:

dockerfile

FROM php:8.2-fpm-alpine

# 换 Alpine 源为清华源
RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apk/repositories

RUN apk add --no-cache nginx supervisor sqlite-dev \
    && docker-php-ext-install pdo pdo_sqlite \
    && docker-php-ext-enable pdo_sqlite
# ...

效果: 重建从 16 分钟降到 2-4 分钟。

坑 5:SSH umask 077 导致上传的源码不可读

症状: 上传的所有 .php 文件是 -rw-------(600),容器内 www-data (UID 82) 读不到源码,访问返回 403。

原因: root 用户 SSH 会话的默认 umask 是 077,新建文件只有属主可读。

正解:

bash

cd /volume5/docker/giftbook/

# 所有文件 644,所有目录 755
find . -type f -exec chmod 644 {} \;
find . -type d -exec chmod 755 {} \;

# data 目录给容器完整写权限
chown -R 82:82 data
chmod -R 775 data

坑 6:nginx 屏蔽 data 目录只能靠配置,不能用 .htaccess

背景: 项目里原本在 data/ 和 data/upload_qrcode/ 放了 .htaccess 防下载/防执行。这套机制在 nginx 下完全无效。

正解: 在 nginx/default.conf 里显式写:

nginx

# 屏蔽 data 目录
location ^~ /data/ { deny all; return 404; }

# 屏蔽敏感 PHP 文件
location ~ ^/(config\.php|db\.php|lib_service\.php|migrate_json_to_sqlite\.php)$ {
    deny all; return 404;
}

# 屏蔽 data 下的 .db/.json
location ~* /data/.*\.(db|db-wal|db-shm|json)$ { deny all; return 404; }

四、完整部署流程

4.1 目录准备

bash

sudo mkdir -p /volume5/docker/giftbook/{nginx,php,data/upload_qrcode,data/draft}
cd /volume5/docker/giftbook/

上传 PHP 源码,然后修复权限:

bash

find . -type f -exec chmod 644 {} \;
find . -type d -exec chmod 755 {} \;
chown -R 82:82 data
chmod -R 775 data

4.2 Dockerfile

dockerfile

FROM php:8.2-fpm-alpine

# 换 Alpine 源为清华源,避免 apk add 卡 16 分钟
RUN sed -i 's|dl-cdn.alpinelinux.org|mirrors.tuna.tsinghua.edu.cn|g' /etc/apk/repositories

RUN apk add --no-cache nginx supervisor sqlite-dev \
    && docker-php-ext-install pdo pdo_sqlite \
    && docker-php-ext-enable pdo_sqlite

RUN php -m | grep -q pdo_sqlite

COPY nginx/default.conf /etc/nginx/http.d/default.conf
COPY php/uploads.ini /usr/local/etc/php/conf.d/uploads.ini

WORKDIR /var/www/html
EXPOSE 80

RUN printf '%s\n' \
    '[supervisord]' \
    'nodaemon=true' \
    'user=root' \
    'pidfile=/tmp/supervisord.pid' \
    'logfile=/dev/null' \
    'logfile_maxbytes=0' \
    '' \
    '[program:php-fpm]' \
    'command=/usr/local/sbin/php-fpm -F' \
    'autostart=true' \
    'autorestart=true' \
    'stdout_logfile=/dev/stdout' \
    'stdout_logfile_maxbytes=0' \
    '' \
    '[program:nginx]' \
    'command=nginx -g "daemon off;"' \
    'autostart=true' \
    'autorestart=true' \
    'stdout_logfile=/dev/stdout' \
    'stdout_logfile_maxbytes=0' \
    > /etc/supervisord.conf

CMD ["/usr/bin/supervisord", "-c", "/etc/supervisord.conf"]

4.3 nginx/default.conf

nginx

server {
    listen 80;
    server_name _;
    root /var/www/html;
    index gift_book_main.php index.php;
    charset utf-8;

    # 让 / 直接指向主页面
    location = / {
        rewrite ^ /gift_book_main.php last;
    }

    # 屏蔽 data 目录
    location ^~ /data/ { deny all; return 404; }

    # 屏蔽敏感 PHP 文件
    location ~ ^/(config\.php|db\.php|lib_service\.php|migrate_json_to_sqlite\.php)$ {
        deny all; return 404;
    }

    location ~* /data/.*\.(db|db-wal|db-shm|json)$ { deny all; return 404; }

    # PHP 处理
    location ~ \.php$ {
        try_files $uri =404;
        fastcgi_pass   127.0.0.1:9000;
        fastcgi_index  index.php;
        fastcgi_param  SCRIPT_FILENAME $document_root$fastcgi_script_name;
        include        fastcgi_params;
    }

    # 静态资源缓存
    location ~* \.(jpg|jpeg|png|gif|webp|css|js|ttf|otf|woff2?)$ {
        expires 7d; access_log off;
    }

    location ~ /\. { deny all; }
}

4.4 php/uploads.ini

ini

upload_max_filesize = 10M
post_max_size = 12M
max_execution_time = 120
date.timezone = Asia/Shanghai

注意:不要在这里写 extension=pdo_sqlite——镜像里已经通过 docker-php-ext-enable 加载了,重复加载会报 Module already loaded 警告。

4.5 docker-compose.yml

yaml

version: "3.8"

services:
  giftbook:
    build: .
    container_name: giftbook
    restart: unless-stopped
    ports:
      - "8080:80"
    volumes:
      # 源码只读挂载
      - /volume5/docker/giftbook:/var/www/html:ro
      # data 目录读写挂载
      - /volume5/docker/giftbook/data:/var/www/html/data:rw
      # nginx 配置单独挂载(改配置无需重建镜像)
      - /volume5/docker/giftbook/nginx/default.conf:/etc/nginx/http.d/default.conf:ro
    environment:
      - TZ=Asia/Shanghai

关键点:

  • 源码 :ro —— 容器内无法改代码,防止被入侵后篡改
  • data 单独 :rw —— SQLite 写库、上传二维码、写草稿
  • nginx/default.conf 单独挂载 —— 改完执行 docker exec giftbook nginx -s reload 即可生效

4.6 启动

bash

cd /volume5/docker/giftbook/

docker-compose down 2>/dev/null
docker-compose up -d --build

首次构建 2-4 分钟(走清华源)。构建成功后:

bash

docker ps | grep giftbook
# 应看到:0.0.0.0:8080->80/tcp

五、验证清单

bash

# 1) 主页面 200
curl -I http://localhost:8080/
curl -I http://localhost:8080/gift_book_main.php

# 2) data 目录 404
curl -I http://localhost:8080/data/giftbook.db

# 3) config.php 404
curl -I http://localhost:8080/config.php

# 4) pdo_sqlite 加载
docker exec -it giftbook php -m | grep -i sqlite

# 5) data 可写
docker exec -it giftbook sh -c 'touch /var/www/html/data/_writetest && echo OK && rm /var/www/html/data/_writetest'

期望结果:

验证项状态
/200 OK
/gift_book_main.php200 OK
/data/giftbook.db404
/config.php404
pdo_sqlite已加载
data 写入OK

六、运维命令速查

bash

cd /volume5/docker/giftbook/

# 查看容器状态
docker-compose ps

# 查看日志(Ctrl+C 退出)
docker-compose logs -f giftbook

# 重启
docker-compose restart giftbook

# 停止
docker-compose down

# 启动
docker-compose up -d

# 改了源码后重建
docker-compose up -d --build

# 只改了 nginx 配置(挂载生效,无需重建)
docker exec -it giftbook nginx -s reload

七、备份策略

所有数据都在 /volume5/docker/giftbook/data/:

  • giftbook.db — SQLite 数据库(用户、事务、记录、日志)
  • upload_qrcode/ — 上传的收款二维码
  • draft/ — 临时草稿(可忽略)

用 Hyper Backup 定期备份:

打开 Hyper Backup → 新增 → 文件夹和套件 → 源选择 /volume5/docker/giftbook/data → 目标选另一个存储空间或外接盘。

手动备份:

bash

mkdir -p /volume1/backup/giftbook
cp -r /volume5/docker/giftbook/data /volume1/backup/giftbook/data_$(date +%Y%m%d)

八、经验总结

教训建议
群晖 Docker ≠ 标准 Linux Docker配置、重启、加速器都走群晖自己的机制
Alpine 包名与 Debian 系不同装包前先查 Alpine 官方包名
容器内 apk 不受 registry 加速器影响直接换 Alpine 镜像源
root 用户 umask 077 会坑容器部署前先 chmod 644 所有源码
.htaccess 在 nginx 下无效必须显式写 location 规则
单容器比双容器省事PHP+SQLite 类应用建议单容器方案
挂载 nginx 配置而非重建镜像改配置立即生效,开发体验提升明显
发布了 13 篇文章 | 共 43.6K 字 | 总访客数: Loading | 总访问量: Loading
Hugo theme Stack designed by Jimmy | 黔ICP备2025049100号-1