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
This commit is contained in:
1 parent
02d904056c
commit
bfe0a0654d
4 files changed
+81
-2
No files matched your search
@@ -0,0 +1,5 @@
|
|||||||
|
.git
|
||||||
|
web/node_modules
|
||||||
|
web/dist
|
||||||
|
CONTEXT.md
|
||||||
|
docs
|
||||||
+30
@@ -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"]
|
||||||
@@ -1 +1,36 @@
|
|||||||
# RawMark
|
# 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/...
|
||||||
|
```
|
||||||
+11
-2
@@ -9,6 +9,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -49,11 +50,11 @@ func run(addr string) error {
|
|||||||
dataDir := env("RAWMARK_DATA", "/data")
|
dataDir := env("RAWMARK_DATA", "/data")
|
||||||
vaultDir := env("RAWMARK_VAULT", filepath.Join(dataDir, "vault"))
|
vaultDir := env("RAWMARK_VAULT", filepath.Join(dataDir, "vault"))
|
||||||
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
if err := os.MkdirAll(dataDir, 0o755); err != nil {
|
||||||
return err
|
return permissionHint(err)
|
||||||
}
|
}
|
||||||
v, err := vault.Open(vaultDir)
|
v, err := vault.Open(vaultDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("打开笔记仓库失败: %w", err)
|
return permissionHint(fmt.Errorf("打开笔记仓库失败: %w", err))
|
||||||
}
|
}
|
||||||
st, err := store.Open(filepath.Join(dataDir, "rawmark.db"))
|
st, err := store.Open(filepath.Join(dataDir, "rawmark.db"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -111,6 +112,14 @@ func run(addr string) error {
|
|||||||
return nil
|
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) {
|
func cleanupSessions(ctx context.Context, st *store.Store) {
|
||||||
t := time.NewTicker(time.Hour)
|
t := time.NewTicker(time.Hour)
|
||||||
defer t.Stop()
|
defer t.Stop()
|
||||||
|
|||||||
Reference in new issue
Block a user