From bfe0a0654d960036fa9c9f25a252774bfceaaa33 Mon Sep 17 00:00:00 2001 From: eezd Date: Fri, 16 Oct 2026 18:37:14 +0800 Subject: [PATCH] build: add Docker image and deployment notes - Multi-stage build: frontend, static Go binary with embedded assets, distroless nonroot runtime (~35 MB) with a built-in healthcheck - Explain how to fix data directory permission errors on bind mounts --- .dockerignore | 5 +++++ Dockerfile | 30 ++++++++++++++++++++++++++++++ README.md | 35 +++++++++++++++++++++++++++++++++++ cmd/rawmark/main.go | 13 +++++++++++-- 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..1a4a1e5 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +.git +web/node_modules +web/dist +CONTEXT.md +docs diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..b1113dc --- /dev/null +++ b/Dockerfile @@ -0,0 +1,30 @@ +# syntax=docker/dockerfile:1 + +# ---- 前端 ---- +FROM node:24-alpine AS web +WORKDIR /src/web +COPY web/package.json web/package-lock.json ./ +RUN npm ci +COPY web/ ./ +RUN npm run build + +# ---- 服务端(前端产物嵌入二进制) ---- +FROM golang:1-alpine AS server +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY cmd ./cmd +COPY internal ./internal +COPY web/embed.go ./web/ +COPY --from=web /src/web/dist ./web/dist +RUN CGO_ENABLED=0 go build -trimpath -ldflags="-s -w" -o /out/rawmark ./cmd/rawmark && mkdir -p /out/data + +# ---- 运行 ---- +FROM gcr.io/distroless/static:nonroot +COPY --from=server /out/rawmark /rawmark +COPY --from=server --chown=nonroot:nonroot /out/data /data +ENV RAWMARK_DATA=/data +EXPOSE 8080 +VOLUME /data +HEALTHCHECK --interval=30s --timeout=5s --start-period=5s CMD ["/rawmark", "healthcheck"] +ENTRYPOINT ["/rawmark"] diff --git a/README.md b/README.md index f776598..e3f5b57 100644 --- a/README.md +++ b/README.md @@ -1 +1,36 @@ # RawMark + +纯 Markdown 笔记网页编辑器:所见即所得(类 Obsidian Live Preview)、文件即真源、Docker 部署、浏览器访问。 + +## 部署 + +```sh +mkdir -p ~/rawmark-data +docker build -t rawmark . +docker run -d --name rawmark \ + --user "$(id -u):$(id -g)" \ + -p 8080:8080 \ + -v ~/rawmark-data:/data \ + --restart unless-stopped \ + rawmark +``` + +- 首次打开需要输入启动码,位于数据目录的 `setup-code.txt`(如 `~/rawmark-data/setup-code.txt`),设置密码后自动删除。 +- 笔记以普通 `.md` 文件保存在 `数据目录/vault/`,图片按 Typora 习惯存放在同级的 `笔记名.assets/`,可直接用 Typora 等编辑器打开;外部修改会实时同步到网页。 +- `--user` 让容器以宿主机用户身份读写数据目录;不指定时容器以 UID 65532 运行,需要该用户对目录有写权限。 +- 请通过 HTTPS 反向代理(Caddy、Nginx 等)访问,并转发 WebSocket(`/api/v1/events`)。笔记加密等功能依赖浏览器的安全上下文,纯 HTTP 下不可用。 + +| 环境变量 | 默认值 | 说明 | +| --- | --- | --- | +| `RAWMARK_DATA` | `/data` | 数据目录(数据库、默认笔记仓库) | +| `RAWMARK_VAULT` | `$RAWMARK_DATA/vault` | 笔记仓库目录,可指向已有的笔记文件夹 | +| `RAWMARK_ADDR` | `:8080` | 监听地址 | + +## 开发 + +```sh +cd web && npm install +npm run dev # 前端开发服务器,/api 转发到 127.0.0.1:8080 +npm run typecheck && npm test +go test ./internal/... +``` diff --git a/cmd/rawmark/main.go b/cmd/rawmark/main.go index bb75b92..fe4b05f 100644 --- a/cmd/rawmark/main.go +++ b/cmd/rawmark/main.go @@ -9,6 +9,7 @@ import ( "context" "errors" "fmt" + "io/fs" "log/slog" "net" "net/http" @@ -49,11 +50,11 @@ func run(addr string) error { dataDir := env("RAWMARK_DATA", "/data") vaultDir := env("RAWMARK_VAULT", filepath.Join(dataDir, "vault")) if err := os.MkdirAll(dataDir, 0o755); err != nil { - return err + return permissionHint(err) } v, err := vault.Open(vaultDir) if err != nil { - return fmt.Errorf("打开笔记仓库失败: %w", err) + return permissionHint(fmt.Errorf("打开笔记仓库失败: %w", err)) } st, err := store.Open(filepath.Join(dataDir, "rawmark.db")) if err != nil { @@ -111,6 +112,14 @@ func run(addr string) error { return nil } +// permissionHint 为权限错误补充排查提示:Docker 绑定挂载的宿主机目录常常对容器内的非 root 用户不可写 +func permissionHint(err error) error { + if errors.Is(err, fs.ErrPermission) { + return fmt.Errorf("%w(当前 UID %d 对数据目录没有写权限:请用 --user 指定宿主机目录所有者的 UID:GID,或执行 chown 修改目录所有者)", err, os.Getuid()) + } + return err +} + func cleanupSessions(ctx context.Context, st *store.Store) { t := time.NewTicker(time.Hour) defer t.Stop()