便笺: Readest 私有化部署笔记

发表于 2025-07-28 13:48 更新于 2025-07-28 18:58 1612 字 9 min read

Readest 是一款现代开源电子书阅读器,用于沉浸式阅读。在 Github 上有 10.1k stars,可以说是一个非常棒的项目。然而,在GitHub项目页面上,仅提供了构建项目的指南,而未详细说明如何配置项目。然后我翻遍了整个互联网,发现啊,TM 全网没有一篇部署教程,全是介绍 Readest 的介绍推广。内容看起来几乎完全一样,十几篇文章相互抄似的。本文作为我个人私有化部署的笔记,目前仅完成了网页端的部署工作,仅供参考之用。

Readest 是一款现代开源电子书阅读器,用于沉浸式阅读。

在 Github 上有 10.1k stars,可以说是一个非常棒的项目。

然而,在GitHub项目页面上,仅提供了构建项目的指南,而未详细说明如何配置项目。然后我翻遍了整个互联网,发现啊,TM 全网没有一篇部署教程,全是介绍 Readest 的介绍推广。内容看起来几乎完全一样,十几篇文章相互抄似的。

本文作为我个人私有化部署的笔记,目前仅完成了网页端的部署工作,仅供参考之用。

我的部署环境

Netlify Build

Runtime: Next.js Base directory: / Package directory: Not set Build command: git submodule update --init --recursive && pnpm i && pnpm --filter @readest/readest-app setup-pdfjs && export NODE_OPTIONS=--max-old-space-size=4096 && pnpm --filter @readest/readest-app build-web Publish directory: apps/readest-app/.next

Node.js: 22.x Build image: Ubuntu Noble 24.04 (default)

ENV

项目中包含一个名为 .env.local.example 的文件,位于 apps/readest-app/.env.local.example。然而,该示例文件的内容并不完整。整个项目共依赖45个环境变量,而该示例文件仅提供了20个。请参照以下配置进行补充:

以下是必须配置的环境变量列表:

  • NEXT_PUBLIC_API_BASE_URL
  • NEXT_PUBLIC_POSTHOG_KEY
  • NEXT_PUBLIC_POSTHOG_HOST
  • NEXT_PUBLIC_SUPABASE_URL
  • NEXT_PUBLIC_SUPABASE_ANON_KEY
  • SUPABASE_ADMIN_KEY
# ====== 核心应用配置 ======
NEXT_PUBLIC_APP_PLATFORM=web # 建议不要填它 应用平台: web/tauri
NEXT_PUBLIC_API_BASE_URL=https://your-api-base-url.com # API基础URL
NEXT_PUBLIC_NODE_BASE_URL= # Node.js服务基础URL(可选)

# ====== 分析监控 ======
NEXT_PUBLIC_POSTHOG_KEY=YOUR_POSTHOG_KEY # PostHog分析密钥
NEXT_PUBLIC_POSTHOG_HOST=YOUR_POSTHOG_HOST # PostHog服务地址
NEXT_PUBLIC_DEFAULT_POSTHOG_URL_BASE64= # PostHog备用URL(base64编码)
NEXT_PUBLIC_DEFAULT_POSTHOG_KEY_BASE64= # PostHog备用密钥(base64编码)

# ====== 数据库 & 认证 ======
NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL # Supabase项目URL
NEXT_PUBLIC_SUPABASE_ANON_KEY=YOUR_SUPABASE_ANON_KEY # Supabase匿名密钥
SUPABASE_ADMIN_KEY=YOUR_SUPABASE_ADMIN_KEY # Supabase管理员密钥
NEXT_PUBLIC_DEFAULT_SUPABASE_URL_BASE64= # 备用Supabase URL(base64)
NEXT_PUBLIC_DEFAULT_SUPABASE_KEY_BASE64= # 备用Supabase密钥(base64)

# ====== 支付 & 订阅 ======
# Stripe配置
STRIPE_SECRET_KEY= # Stripe生产环境密钥
STRIPE_SECRET_KEY_DEV= # Stripe开发环境密钥
STRIPE_WEBHOOK_SECRET= # Stripe webhook验证密钥
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_BASE64= # Stripe生产公钥(base64)
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY_DEV_BASE64= # Stripe开发公钥(base64)

# Apple应用内支付
APPLE_IAP_KEY_ID= # Apple IAP密钥ID
APPLE_IAP_ISSUER_ID= # Apple IAP发行者ID
APPLE_IAP_BUNDLE_ID= # Apple应用包ID
APPLE_IAP_PRIVATE_KEY_BASE64= # Apple私钥(base64编码)

# ====== 文件存储 ======
# 存储类型: r2/s3
NEXT_PUBLIC_OBJECT_STORAGE_TYPE=r2 

# Cloudflare R2配置
R2_ACCESS_KEY_ID=YOUR_R2_ACCESS_KEY_ID
R2_SECRET_ACCESS_KEY=YOUR_R2_SECRET_ACCESS_KEY
R2_BUCKET_NAME=YOUR_R2_BUCKET_NAME
R2_ACCOUNT_ID=YOUR_R2_ACCOUNT_ID
R2_REGION=auto # R2存储区域

# AWS S3配置
S3_ENDPOINT= # S3服务端点
S3_ACCESS_KEY_ID= # S3访问密钥
S3_SECRET_ACCESS_KEY= # S3密钥
S3_BUCKET_NAME= # S3存储桶名称
S3_REGION= # S3存储区域

# ====== 资源配额 ======
NEXT_PUBLIC_STORAGE_FIXED_QUOTA=1073741824 # 固定存储配额(字节)
NEXT_PUBLIC_TRANSLATION_FIXED_QUOTA= # 固定翻译配额(可选)

# ====== 翻译服务 ======
DEEPL_PRO_API_KEYS=YOUR_DEEPL_PRO_API_KEYS # DeepL专业版API密钥
DEEPL_FREE_API_KEYS=YOUR_DEEPL_FREE_API_KEYS # DeepL免费版API密钥
DEEPL_X_FINGERPRINT= # DeepL安全指纹
DEEPL_PRO_API= # DeepL专业版API端点
DEEPL_FREE_API= # DeepL免费版API端点

# ====== 高级功能 ======
NEXT_PUBLIC_USE_APPLE_SIGN_IN=false # 启用Apple登录
NEXT_PUBLIC_DISABLE_UPDATER= # 禁用自动更新
NEXT_PUBLIC_DIST_CHANNEL=readest # 发布渠道
ENABLE_IAP_API_TESTS=false # 启用IAP测试
USE_CUSTOM_OAUTH= # 使用自定义OAuth

# ====== 服务器配置 ======
PROTOCOL=http # 服务协议: http/https
HOST=localhost:3000 # 服务主机
ANALYZE=false # 启用bundle分析
R2_TOKEN_VALUE=YOUR_R2_TOKEN_VALUE # R2访问令牌(可选)

数据库 Supabase 配置

创建数据库表

执行以下 SQL 语句以创建表。

官方的 Supabase Tables Schema for Sync APIpublic.books 表缺少了两列 source_title metadatatransform.ts (apps/readest-app/src/utils/transform.ts) 会尝试向这两列写入数据,public.books 表缺少这两列直接报错。

总之运行我这段 SQL 就行了。

create table public.books (
  user_id uuid not null,
  book_hash text not null,
  format text null, -- 'EPUB' | 'PDF' | 'MOBI' | 'CBZ' | 'FB2' | 'FBZ'
  title text null,
  source_title text null,
  author text null,
  "group" text null,
  tags text[] null,
  metadata text null,
  created_at timestamp with time zone null default now(),
  updated_at timestamp with time zone null default now(),
  deleted_at timestamp with time zone null,
  uploaded_at timestamp with time zone null,
  progress integer[] null,
  group_id text null,
  group_name text null,
  constraint books_pkey primary key (user_id, book_hash),
  constraint books_user_id_fkey foreign KEY (user_id) references auth.users (id) on delete CASCADE
) TABLESPACE pg_default;

ALTER TABLE public.books ENABLE ROW LEVEL SECURITY;

CREATE POLICY select_books ON public.books
  FOR SELECT to authenticated USING ((select auth.uid()) = user_id);
CREATE POLICY insert_books ON public.books
  FOR INSERT to authenticated WITH CHECK ((select auth.uid()) = user_id);
CREATE POLICY update_books ON public.books
  FOR UPDATE to authenticated USING ((select auth.uid()) = user_id);
CREATE POLICY delete_books ON public.books
  FOR DELETE to authenticated USING ((select auth.uid()) = user_id);

create table public.book_configs (
  user_id uuid not null,
  book_hash text not null,
  location text null,
  progress jsonb null,
  search_config jsonb null,
  view_settings jsonb null,
  created_at timestamp with time zone null default now(),
  updated_at timestamp with time zone null default now(),
  deleted_at timestamp with time zone null,
  constraint book_configs_pkey primary key (user_id, book_hash),
  constraint book_configs_user_id_fkey foreign KEY (user_id) references auth.users (id) on delete CASCADE
) TABLESPACE pg_default;

ALTER TABLE public.book_configs ENABLE ROW LEVEL SECURITY;

CREATE POLICY select_book_configs ON public.book_configs
  FOR SELECT to authenticated USING ((select auth.uid()) = user_id);
CREATE POLICY insert_book_configs ON public.book_configs
  FOR INSERT to authenticated WITH CHECK ((select auth.uid()) = user_id);
CREATE POLICY update_book_configs ON public.book_configs
  FOR UPDATE to authenticated USING ((select auth.uid()) = user_id);
CREATE POLICY delete_book_configs ON public.book_configs
  FOR DELETE to authenticated USING ((select auth.uid()) = user_id);

create table public.book_notes (
  user_id uuid not null,
  book_hash text not null,
  id text not null,
  type text null,
  cfi text null,
  text text null,
  style text null,
  color text null,
  note text null,
  created_at timestamp with time zone null default now(),
  updated_at timestamp with time zone null default now(),
  deleted_at timestamp with time zone null,
  constraint book_notes_pkey primary key (user_id, book_hash, id),
  constraint book_notes_user_id_fkey foreign KEY (user_id) references auth.users (id) on delete CASCADE
) TABLESPACE pg_default;

ALTER TABLE public.book_notes ENABLE ROW LEVEL SECURITY;

CREATE POLICY select_book_notes ON public.book_notes
  FOR SELECT to authenticated USING ((select auth.uid()) = user_id);
CREATE POLICY insert_book_notes ON public.book_notes
  FOR INSERT to authenticated WITH CHECK ((select auth.uid()) = user_id);
CREATE POLICY update_book_notes ON public.book_notes
  FOR UPDATE to authenticated USING ((select auth.uid()) = user_id);
CREATE POLICY delete_book_notes ON public.book_notes
  FOR DELETE to authenticated USING ((select auth.uid()) = user_id);

-- Create the `files` table
create table public.files (
  id uuid not null default gen_random_uuid (),
  user_id uuid not null,
  book_hash text null,
  file_key text not null,
  file_size bigint not null,
  created_at timestamp with time zone null default now(),
  deleted_at timestamp with time zone null,
  constraint files_pkey primary key (id),
  constraint files_file_key_key unique (file_key),
  constraint files_user_id_fkey foreign KEY (user_id) references auth.users (id) on delete CASCADE
) TABLESPACE pg_default;

-- Add an index for efficient querying by user_id and deleted_at
create index idx_files_user_id_deleted_at
on public.files (user_id, deleted_at);

create index idx_files_file_key
on public.files (file_key);

create index idx_files_file_key_deleted_at
on public.files (file_key, deleted_at);

-- Enable RLS on the `files` table
alter table public.files enable row level security;

create policy "Users can insert their own files"
on public.files
for insert
with check (
  auth.uid() = user_id
);


create policy "Users can view their own active files"
on public.files
for select
using (
  auth.uid() = user_id and deleted_at is null
);


create policy "Users can soft-delete their own files"
on public.files
for update
using (
  auth.uid() = user_id
)
with check (
  deleted_at is null or deleted_at > now()
);

create policy "Users can delete their own files permanently"
on public.files
for delete
using (
  auth.uid() = user_id
);

配置 Supabase 中的 site URL

在 Supabase 仪表盘中找到 Authentication(左侧) > site URL 把 site URL 改为网站 URL

配置用户

Readest 的用户是在 Supabase 管理的,可以在 Authentication > Users 新增用户,删除用户。

在 Authentication > Policies 可以设置登录/注册方式 (Email/Google/Github/Apple),但 Readest 还是会在登录界面显示这些选项,只是禁用的方式登录失败。

还可以在 Authentication > Policies 禁用用户注册。

我这是全程在手机上操作的,手机巨垃圾。 天玑 9300+ 16.0GB 运行内存,我怎么都想不明白在 Termux 调试怎么卡得要命,网页一离开前台就给关掉了。手机性能超差。

喜欢的话,留下你的评论吧~