mdserve 使用 Podman Compose 部署 Markdown 在线预览服务

1. 目标

使用 mdserve 将服务器上的 Markdown 文件以网页形式展示,实现:

  • Markdown 文件直接通过浏览器访问
  • 默认打开 README.md
  • 支持 Markdown、图片、代码块等内容
  • 使用 Alpine Linux 作为运行环境
  • 使用 Docker multi-stage build 构建镜像
  • 使用 Podman / podman-compose 运行
  • Markdown 文件放在宿主机目录,不打包进镜像
  • 可以选择是否开启 Markdown 文件自动更新
  • 可以选择关闭 mdserve 的版本更新检查
  • 最终通过 http://服务器IP:9000/ 访问

整体结构:

宿主机
/data/docs
    |
    | volume,只读挂载
    v
Podman 容器
/data/docs
    |
    v
mdserve
    |
    v
0.0.0.0:9000
    |
    v
浏览器

2. Windows 上的 mdserve

之前在 Windows 上使用:

G:\ProgramFiles\mdserve-main\mdserve.exe serve \
  --dir "G:\ProgramFiles\WorkBuddyfile\workbu0811\a1" \
  --default-doc README.md

启动后显示类似:

┌────────────────────────────────────────┐
│   mdserve · browse markdown as html    │
└────────────────────────────────────────┘

v0.7.0   ● latest

G:\ProgramFiles\WorkBuddyfile\workbu0811\a1

4 markdown files · 1 directory

➜  http://127.0.0.1:8080/

Windows 使用的是 8080 端口。

Linux 部署时可以使用另外一个端口,例如 9000


3. mdserve 安装脚本

mdserve 官方提供安装脚本:

curl -fsSL https://raw.githubusercontent.com/mostafa-re/mdserve/main/scripts/install.sh | sh

实际构建时发现,该脚本会自动识别当前系统架构。

例如当前服务器是 x86_64 / amd64 时:

mdserve: downloading v0.7.0 (linux/amd64)

安装完成后实际路径为:

/root/.local/bin/mdserve

而不是:

/usr/local/bin/mdserve

这一点在制作 Dockerfile 时非常重要。


4. 最初的 Dockerfile

最开始可以直接在 Alpine 中安装:

FROM docker.1ms.run/alpine:3.22

RUN apk add --no-cache \
    ca-certificates \
    curl

RUN curl -fsSL https://raw.githubusercontent.com/mostafa-re/mdserve/main/scripts/install.sh | sh

RUN mkdir -p /data/docs

EXPOSE 9000

ENTRYPOINT ["/usr/local/bin/mdserve"]

CMD ["serve", \
     "--dir", "/data/docs", \
     "--addr", "0.0.0.0:9000", \
     "--default-doc", "README.md"]

但是这个版本存在一个问题:

安装脚本实际将 mdserve 安装到了:

/root/.local/bin/mdserve

因此:

ENTRYPOINT ["/usr/local/bin/mdserve"]

并不能直接使用安装脚本生成的文件。

另外,最终运行镜像其实不需要 curl,因此更适合使用 multi-stage build。


5. 推荐使用 Multi-stage Build

多阶段构建的基本思路:

第一阶段 builder
    |
    | 安装 curl
    | 下载 mdserve
    | 得到 mdserve 二进制文件
    |
    v
第二阶段 runtime
    |
    | 使用干净的 Alpine
    | 只复制 mdserve 二进制
    |
    v
最终镜像

这样最终运行镜像不需要包含:

curl
安装脚本
builder 阶段的其他文件

最终镜像更加干净。


6. 推荐最终 Dockerfile

当前使用的推荐版本:

# ==============================
# 构建阶段
# ==============================
FROM docker.1ms.run/alpine:3.22 AS builder

RUN apk add --no-cache \
    ca-certificates \
    curl

# 安装 mdserve
RUN curl -fsSL https://raw.githubusercontent.com/mostafa-re/mdserve/main/scripts/install.sh | sh


# ==============================
# 运行阶段
# ==============================
FROM docker.1ms.run/alpine:3.22

RUN apk add --no-cache ca-certificates

# mdserve 实际安装位置是 /root/.local/bin/mdserve
COPY --from=builder /root/.local/bin/mdserve /usr/local/bin/mdserve

RUN mkdir -p /data/docs

EXPOSE 9000

ENTRYPOINT ["/usr/local/bin/mdserve"]

CMD ["serve", \
     "--dir", "/data/docs", \
     "--addr", "0.0.0.0:9000", \
     "--default-doc", "README.md"]

这里最关键的一行是:

COPY --from=builder /root/.local/bin/mdserve /usr/local/bin/mdserve

含义:

builder:
/root/.local/bin/mdserve
        |
        | COPY --from=builder
        v
runtime:
/usr/local/bin/mdserve

然后:

ENTRYPOINT ["/usr/local/bin/mdserve"]

就可以正常启动。


7. 为什么之前会出现 COPY 错误

之前使用:

COPY --from=builder /usr/local/bin/mdserve /usr/local/bin/mdserve

构建时报错:

Error: building at STEP "COPY --from=builder /usr/local/bin/mdserve /usr/local/bin/mdserve":

copier: stat:
"/usr/local/bin/mdserve":
no such file or directory

原因不是 Podman 或 Alpine 出问题。

而是 builder 阶段实际安装到了:

/root/.local/bin/mdserve

日志已经明确显示:

mdserve: installed v0.7.0 to /root/.local/bin/mdserve

所以应该使用:

COPY --from=builder /root/.local/bin/mdserve /usr/local/bin/mdserve

8. 构建镜像

假设 Dockerfile 位于:

/data/docsify/zd1/Dockerfile

进入目录:

cd /data/docsify/zd1

使用 Podman 构建:

podman build -t mdserve:latest .

如果需要完全重新构建:

podman build --no-cache -t mdserve:latest .

构建完成后检查:

podman images

应该能够看到:

mdserve    latest

9. 检查 mdserve 是否正常

构建完成后,可以直接执行:

podman run --rm mdserve:latest --version

如果安装成功,应当能够看到类似:

mdserve v0.7.0

这一步可以在正式创建容器之前验证镜像。


10. Markdown 文件不要放进镜像

生产环境推荐:

镜像
    |
    | 只负责 mdserve 程序
    |
    v
容器

宿主机
/data/docs
    |
    | volume
    v
容器 /data/docs

不要把 Markdown 文件写死在 Dockerfile 中。

这样以后上传新的 .md 文件时:

不需要重新 build
不需要重新制作镜像
不需要重新下载 mdserve

只需要把文件放进:

/data/docs

即可。

例如:

/data/docs/
├── README.md
├── guide.md
├── api.md
├── install.md
└── images/
    ├── logo.png
    └── demo.png

11. Docker volume 挂载

使用:

-v /data/docs:/data/docs:ro

含义:

宿主机:
/data/docs

        |
        | 只读挂载
        v

容器:
/data/docs

其中:

/data/docs

前面的是宿主机路径。

/data/docs

后面的是容器内部路径。

:ro

表示 Read Only,只读。

因此:

  • mdserve 可以读取 Markdown
  • mdserve 可以读取图片
  • 容器不能修改 Markdown
  • 容器不能删除 Markdown
  • 宿主机仍然可以正常修改和上传文件

对于文档服务器而言,这种配置比较合适。


12. Podman run 方式

如果不使用 compose,可以直接:

podman run -d \
  --name mdserve \
  -p 9000:9000 \
  -v /data/docs:/data/docs:ro \
  --restart unless-stopped \
  mdserve:latest \
  serve \
  --dir /data/docs \
  --addr 0.0.0.0:9000 \
  --default-doc README.md \
  --no-update-check

这里:

9000:9000

表示:

宿主机 9000
    |
    v
容器 9000

浏览器访问:

http://服务器IP:9000/

13. Dockerfile 中 CMD 与 podman run 参数的关系

Dockerfile 中:

ENTRYPOINT ["/usr/local/bin/mdserve"]

CMD ["serve", \
     "--dir", "/data/docs", \
     "--addr", "0.0.0.0:9000", \
     "--default-doc", "README.md"]

最终组合起来相当于:

/usr/local/bin/mdserve \
    serve \
    --dir /data/docs \
    --addr 0.0.0.0:9000 \
    --default-doc README.md

如果在 podman run 后面提供参数:

mdserve:latest \
  serve \
  --dir /data/docs \
  --addr 0.0.0.0:9000 \
  --default-doc README.md \
  --no-update-check

这些参数会覆盖 Dockerfile 中的 CMD

因此:

ENTRYPOINT

负责确定运行哪个程序。

而:

CMD

负责提供默认参数。


14. Podman Compose 推荐配置

推荐使用 compose.yml

services:
  mdserve:
    image: mdserve:latest
    container_name: mdserve

    restart: unless-stopped

    ports:
      - "9000:9000"

    volumes:
      - /data/docs:/data/docs:ro

    command:
      - serve
      - --dir
      - /data/docs
      - --addr
      - 0.0.0.0:9000
      - --default-doc
      - README.md
      - --no-update-check

如果使用已有的外部网络:

services:
  mdserve:
    image: mdserve:latest
    container_name: mdserve

    environment:
      TZ: Asia/Shanghai

    restart: unless-stopped

    ports:
      - "9000:9000"

    volumes:
      - /data/docs:/data/docs:ro

    networks:
      - znet

    command:
      - serve
      - --dir
      - /data/docs
      - --addr
      - 0.0.0.0:9000
      - --default-doc
      - README.md
      - --no-update-check

networks:
  znet:
    external: true

如果 znet 不存在,需要先创建:

podman network create znet

然后:

podman-compose up -d

15. 推荐让 compose 自动构建镜像

如果希望 podman-compose 同时负责构建,可以:

services:
  mdserve:
    build:
      context: /data/docsify/zd1
      dockerfile: Dockerfile

    image: mdserve:latest
    container_name: mdserve

    environment:
      TZ: Asia/Shanghai

    restart: unless-stopped

    ports:
      - "9000:9000"

    volumes:
      - /data/docs:/data/docs:ro

    command:
      - serve
      - --dir
      - /data/docs
      - --addr
      - 0.0.0.0:9000
      - --default-doc
      - README.md
      - --no-update-check

然后:

podman-compose up -d --build

以后 Dockerfile 有修改时:

podman-compose up -d --build

如果只是修改:

/data/docs/*.md

则不需要重新 build。


16. 相对路径和绝对路径

之前 compose 中使用:

volumes:
  - ./docs:/data/docs:ro

这个路径是相对于执行 podman-compose 时的 compose 项目目录。

例如如果当前目录:

/data/caddy

那么:

- ./docs:/data/docs:ro

实际对应:

/data/caddy/docs

而不是:

/data/docs

这很容易导致:

0 markdown files

17. 推荐使用绝对路径

为了避免路径错误,推荐:

volumes:
  - /data/docs:/data/docs:ro

这样无论从哪个目录执行:

podman-compose up -d

都明确表示:

宿主机 /data/docs
        |
        v
容器 /data/docs

18. 排查 mdserve 显示 0 markdown files

如果日志出现:

/data/docs
0 markdown files · 1 directory

首先检查宿主机:

ls -lah /data/docs

应该能够看到:

README.md
xxx.md

然后检查容器:

podman exec -it mdserve ls -lah /data/docs

如果容器内部也能看到:

README.md

说明挂载正常。

如果宿主机有文件,但是容器里面没有,则重点检查 compose 的 volumes

推荐直接使用:

volumes:
  - /data/docs:/data/docs:ro

修改 compose 后重新创建:

podman-compose down
podman-compose up -d

19. Linux 端口

Windows 原来的 mdserve 使用:

http://127.0.0.1:8080/

Linux 当前使用:

--addr 0.0.0.0:9000

并且 compose:

ports:
  - "9000:9000"

所以浏览器访问:

http://服务器IP:9000/

例如:

http://1.2.3.4:9000/

对应关系:

浏览器
    |
    | http://服务器IP:9000
    v
宿主机 9000
    |
    v
Podman 容器 9000
    |
    v
mdserve

20. 如果希望外部继续使用 8080

也可以不修改 mdserve 容器内部端口。

只需要:

ports:
  - "8080:9000"

含义:

宿主机 8080
    |
    v
容器 9000
    |
    v
mdserve

此时浏览器访问:

http://服务器IP:8080/

因此:

9000:9000

表示外部使用 9000。

而:

8080:9000

表示外部使用 8080,容器内部仍然使用 9000。


21. mdserve 的 --no-reload

默认情况下不加:

--no-reload

mdserve 会进行 live reload。

浏览器会周期性访问:

/api/poll

例如:

https://zdoc.zhaopeng.site/api/poll

可以在浏览器开发者工具 Network 中看到这个请求周期性出现。

这是 mdserve 的正常行为,不是异常流量。


22. 为什么上传 Markdown 后网页会自动更新

当前没有:

--no-reload

所以工作流程是:

上传 README.md
        |
        v
/data/docs/README.md 发生变化
        |
        v
浏览器请求 /api/poll
        |
        v
mdserve 检测到文件变化
        |
        v
网页自动更新

因此之前观察到:

上传 .md 文件
    |
    v
网页自动更新

说明 live reload 正常工作。


23. --no-reload 的作用

如果加入:

--no-reload

则关闭 live reload。

浏览器不会持续通过:

/api/poll

检查 Markdown 文件变化。

效果:

上传 README.md
        |
        v
网页不会自动更新
        |
        v
手动刷新浏览器
        |
        v
看到最新内容

24. --no-update-check 的作用

这个参数:

--no-update-check

与 Markdown 自动刷新没有关系。

它用于关闭 mdserve 的版本更新检查。

生产环境一般可以使用:

--no-update-check

这样可以减少不必要的更新检查请求。


25. 两个参数的区别

参数作用是否影响 Markdown 自动更新
--no-reload关闭 live reload
--no-update-check关闭版本更新检查

如果需要实时编辑预览:

不加 --no-reload

如果是生产环境,只提供已经写好的文档:

可以加 --no-reload

通常生产环境可以:

--no-reload
--no-update-check

26. 两种推荐运行方式

开发 / 编辑文档环境

command:
  - serve
  - --dir
  - /data/docs
  - --addr
  - 0.0.0.0:9000
  - --default-doc
  - README.md
  - --no-update-check

特点:

  • 支持自动检测 Markdown 文件变化
  • 上传 .md 后网页自动更新
  • 浏览器会周期性访问 /api/poll
  • 适合边编辑边预览

生产 / 稳定展示环境

command:
  - serve
  - --dir
  - /data/docs
  - --addr
  - 0.0.0.0:9000
  - --default-doc
  - README.md
  - --no-reload
  - --no-update-check

特点:

  • 不进行 live reload
  • 浏览器不需要持续轮询 /api/poll
  • 上传 Markdown 后手动刷新
  • 网络请求更少
  • 更适合纯文档展示

27. 当前场景推荐配置

如果这个网站主要用于:

服务器上放 Markdown 文档,然后通过网页查看。

推荐:

services:
  mdserve:
    image: mdserve:latest
    container_name: mdserve

    environment:
      TZ: Asia/Shanghai

    restart: unless-stopped

    ports:
      - "9000:9000"

    volumes:
      - /data/docs:/data/docs:ro

    command:
      - serve
      - --dir
      - /data/docs
      - --addr
      - 0.0.0.0:9000
      - --default-doc
      - README.md
      - --no-reload
      - --no-update-check

如果经常上传 Markdown,并希望上传后浏览器自动更新,则删除:

- --no-reload

保留:

- --no-update-check

28. Caddyfile 代码高亮警告

浏览器控制台曾出现:

highlight.min.js:107 WARN:
Could not find the language 'Caddyfile',
did you forget to load/include a language module?

以及:

Falling back to no-highlight mode for this block.

这不是 mdserve 服务端错误。

原因是 Markdown 中存在类似:

```Caddyfile
example.com {
    reverse_proxy localhost:9000
}
```

mdserve 使用的前端 highlight.js 没有加载 Caddyfile 对应的语言模块。

因此该代码块会:

不进行语法高亮

但:

代码内容仍然可以正常显示

所以:

  • 不影响 mdserve
  • 不影响 Markdown 页面
  • 不影响 Caddy
  • 不影响反向代理
  • 不影响其他代码块

如果希望消除这个警告,可以把:

```Caddyfile

改成:

```text

或者直接:

```

这样会以普通文本形式显示。

如果只是偶尔出现,通常可以忽略。


29. 当前完整架构

最终可以整理成:

                    浏览器
                       |
                       | HTTP :9000
                       v
              +------------------+
              |      Caddy       |
              |  可选反向代理     |
              +------------------+
                       |
                       | proxy
                       v
              +------------------+
              |     Podman       |
              |                  |
              |    mdserve       |
              |    :9000         |
              |                  |
              |    /data/docs    |
              +------------------+
                       ^
                       |
                  volume :ro
                       |
                       |
              /data/docs
              宿主机 Markdown

如果使用 Caddy 域名:

https://zdoc.zhaopeng.site
        |
        v
      Caddy
        |
        v
127.0.0.1:9000
        |
        v
    mdserve
        |
        v
 /data/docs/*.md

30. 推荐目录结构

推荐服务器目录:

/data/
├── docs/
│   ├── README.md
│   ├── guide.md
│   ├── api.md
│   └── images/
│       ├── logo.png
│       └── demo.png
│
└── docsify/
    └── zd1/
        ├── Dockerfile
        └── compose.yml

其中:

/data/docs

负责存放实际 Markdown 文档。

/data/docsify/zd1

负责 mdserve 镜像和 compose 配置。


31. 常用命令

构建镜像

podman build -t mdserve:latest .

强制重新构建

podman build --no-cache -t mdserve:latest .

查看镜像

podman images

查看 mdserve 版本

podman run --rm mdserve:latest --version

启动 compose

podman-compose up -d

构建并启动

podman-compose up -d --build

查看 compose 配置

podman-compose config

查看容器

podman ps

查看日志

podman logs mdserve

实时查看日志

podman logs -f mdserve

查看容器中的 Markdown

podman exec -it mdserve ls -lah /data/docs

进入容器

podman exec -it mdserve sh

停止 compose

podman-compose down

重启

podman-compose restart

删除容器后重新创建

podman-compose down
podman-compose up -d

32. 最终推荐方案

Dockerfile

FROM docker.1ms.run/alpine:3.22 AS builder

RUN apk add --no-cache \
    ca-certificates \
    curl

RUN curl -fsSL https://raw.githubusercontent.com/mostafa-re/mdserve/main/scripts/install.sh | sh


FROM docker.1ms.run/alpine:3.22

RUN apk add --no-cache ca-certificates

COPY --from=builder /root/.local/bin/mdserve /usr/local/bin/mdserve

RUN mkdir -p /data/docs

EXPOSE 9000

ENTRYPOINT ["/usr/local/bin/mdserve"]

CMD ["serve", \
     "--dir", "/data/docs", \
     "--addr", "0.0.0.0:9000", \
     "--default-doc", "README.md"]

compose.yml

如果希望生产环境关闭自动刷新:

services:
  mdserve:
    image: mdserve:latest
    container_name: mdserve

    environment:
      TZ: Asia/Shanghai

    restart: unless-stopped

    ports:
      - "9000:9000"

    volumes:
      - /data/docs:/data/docs:ro

    command:
      - serve
      - --dir
      - /data/docs
      - --addr
      - 0.0.0.0:9000
      - --default-doc
      - README.md
      - --no-reload
      - --no-update-check

如果希望上传 Markdown 后自动更新:

services:
  mdserve:
    image: mdserve:latest
    container_name: mdserve

    environment:
      TZ: Asia/Shanghai

    restart: unless-stopped

    ports:
      - "9000:9000"

    volumes:
      - /data/docs:/data/docs:ro

    command:
      - serve
      - --dir
      - /data/docs
      - --addr
      - 0.0.0.0:9000
      - --default-doc
      - README.md
      - --no-update-check

33. 一套完整部署流程

进入 Dockerfile 所在目录:

cd /data/docsify/zd1

构建:

podman build -t mdserve:latest .

检查:

podman images | grep mdserve

确认 Markdown:

ls -lah /data/docs

如果使用外部网络:

podman network create znet

如果已经存在则不需要重复创建。

启动:

podman-compose up -d

检查:

podman ps

查看日志:

podman logs mdserve

检查容器中的文件:

podman exec -it mdserve ls -lah /data/docs

最后浏览器访问:

http://服务器IP:9000/

如果前面配置了 Caddy 反向代理,则直接访问:

https://你的域名/

34. 最终结论

当前部署方案的核心原则是:

Docker 镜像
    =
mdserve 程序

宿主机 /data/docs
    =
实际 Markdown 数据

Podman volume
    =
连接两者

Podman Compose
    =
管理容器运行参数

Caddy
    =
可选的 HTTPS / 域名 / 反向代理层

这种方式最大的优点是:

修改 Markdown
    ↓
不需要重新构建镜像

上传 Markdown
    ↓
不需要重新创建容器

升级 mdserve
    ↓
只需要重新构建镜像

修改端口或 mdserve 参数
    ↓
修改 compose.yml 即可

对于长期运行的 Markdown 文档网站,这种架构比把 .md 文件直接 COPY 进 Docker 镜像更加方便。

标签: none

添加新评论