fix: 修复 docker 方式部署宿主机数据库目录权限

This commit is contained in:
nap0o committed 2026-03-13 04:58:57 +08:00
1 parent 9b49dade41
commit 1c1d34df02
8 files changed
+81 -22

No files matched your search

+1 -1
View File
@@ -41,4 +41,4 @@ USER node
EXPOSE 3000
# Start the server
CMD ["node", "backend/dist/server.js"]
CMD ["node", "backend/dist/server.js"]
+15 -4
View File
@@ -91,8 +91,18 @@
如果你想把数据完全掌控在自己手里,或者需要在内网环境运行。
**⚠️ 核心安全须知**:为了提供最高级别的安全保障,本项目的 Docker 镜像严格遵循**非特权用户(Rootless)**运行标准(内部以 `node` 用户,UID 1000 运行),从而有效防止潜在的容器逃逸风险。
在运行容器前,你**必须**预先在宿主机创建目录并分配正确的权限,否则会出现 `Permission Denied` 报错。
#### 0. 准备数据目录 (必做)
在你想运行容器的目录下,执行以下命令:
```bash
mkdir -p data && sudo chown -R 1000:1000 data
```
#### 1. 使用 Docker Compose (推荐)
在你的服务器上创建 `docker-compose.yml`:
1. 在服务器上创建 `docker-compose.yml`:
```yaml
services:
2fauth:
@@ -103,6 +113,7 @@ services:
volumes:
- ./data:/app/data
environment:
# 核心密钥
- ENCRYPTION_KEY=32位以上随机密钥
- JWT_SECRET=32位以上随机JWT密钥
- OAUTH_ALLOWED_USERS=你的邮箱@example.com
@@ -112,7 +123,7 @@ services:
- OAUTH_GITHUB_REDIRECT_URI=https://你的域名/oauth/callback
restart: unless-stopped
```
运行 `docker compose up -d` 即可。
2. 运行 `docker compose up -d` 即可。
#### 2. 使用 Docker Run
```bash
@@ -124,8 +135,8 @@ docker run -d --name 2fauth-worker \
-e OAUTH_ALLOWED_USERS=你的邮箱 \
-e OAUTH_GITHUB_CLIENT_ID= \
-e OAUTH_GITHUB_CLIENT_SECRET= \
-e OAUTH_GITHUB_CLIENT_REDIRECT_URI= \
nap0o/2fauth-worker:latest
-e OAUTH_GITHUB_REDIRECT_URI=https://你的域名/oauth/callback \
nap0o/2fauth-worker:latest
```
---
+16 -6
View File
@@ -93,8 +93,17 @@ The most hassle-free way. No server required; Cloudflare hosts it for you for fr
If you want absolute control over your data or need to run it in a local network.
**⚠️ Important Security Notice**: To provide the highest level of security, this image runs strictly as a **non-privileged user (node, UID 1000)** to prevent container escape risks.
You **MUST** create the data directory and set the correct permissions on your host machine before starting the container, otherwise you will encounter `Permission Denied` errors.
#### 0. Prepare Data Directory (Required)
```bash
mkdir -p data && sudo chown -R 1000:1000 data
```
#### 1. Using Docker Compose (Recommended)
Create `docker-compose.yml` on your server:
1. Create `docker-compose.yml` on your server:
```yaml
services:
2fauth:
@@ -105,16 +114,17 @@ services:
volumes:
- ./data:/app/data
environment:
# Core Secrets
- ENCRYPTION_KEY=Your_32_Character_Random_Key
- JWT_SECRET=Your_Random_JWT_Secret
- JWT_SECRET=Your_32_Character_Random_JWT_Secret
- OAUTH_ALLOWED_USERS=your_email@example.com
# Configure at least one provider (GitHub example)
- OAUTH_GITHUB_CLIENT_ID=your_id
- OAUTH_GITHUB_CLIENT_SECRET=your_secret
- OAUTH_GITHUB_REDIRECT_URI=https://your-domain.com/oauth/callback
- OAUTH_GITHUB_REDIRECT_URI=[https://your-domain.com/oauth/callback](https://your-domain.com/oauth/callback)
restart: unless-stopped
```
Run `docker compose up -d`.
2. Run `docker compose up -d`.
#### 2. Using Docker Run
```bash
@@ -126,8 +136,8 @@ docker run -d --name 2fauth-worker \
-e OAUTH_ALLOWED_USERS=your_email@example.com \
-e OAUTH_GITHUB_CLIENT_ID= \
-e OAUTH_GITHUB_CLIENT_SECRET= \
-e OAUTH_GITHUB_CLIENT_REDIRECT_URI= \
nap0o/2fauth-worker:latest
-e OAUTH_GITHUB_REDIRECT_URI=[https://your-domain.com/oauth/callback](https://your-domain.com/oauth/callback) \
nap0o/2fauth-worker:latest
```
---
+2 -2
View File
@@ -14,7 +14,7 @@
"drizzle-orm": "^0.30.10",
"hono": "^4.12.3",
"node-cron": "^4.2.1",
"nodemailer": "^6.10.1",
"nodemailer": "^7.0.11",
"webdav": "^5.9.0"
},
"devDependencies": {
@@ -22,7 +22,7 @@
"@types/better-sqlite3": "^7.6.13",
"@types/node": "^25.3.5",
"@types/node-cron": "^3.0.11",
"@types/nodemailer": "^6.4.17",
"@types/nodemailer": "^7.0.11",
"drizzle-kit": "^0.31.9",
"typescript": "^5.9.3",
"wrangler": "^4.71.0"
+34 -2
View File
@@ -18,14 +18,46 @@ const dataDir = path.resolve(baseDir, 'data');
console.log(`[Docker Server] Base directory: ${baseDir}`);
console.log(`[Docker Server] Frontend dist path: ${frontendDistPath}`);
// 2. Ensure data directory exists
// 2. Ensure data directory exists and is writable
if (!fs.existsSync(dataDir)) {
fs.mkdirSync(dataDir, { recursive: true });
}
try {
fs.accessSync(dataDir, fs.constants.W_OK);
} catch (err) {
console.error(`\n❌ ERROR: Data directory "${dataDir}" is NOT writable!`);
console.error(` Please run on your host: sudo chown -R 1000:1000 ./data\n`);
process.exit(1);
}
// 3. Initialize local SQLite Database
const dbFile = process.env.SQLITE_DB_PATH || path.join(dataDir, '2fauth.db');
const sqlite = new Database(dbFile);
// Check if database file is writable if it exists
if (fs.existsSync(dbFile)) {
try {
fs.accessSync(dbFile, fs.constants.W_OK);
} catch (err) {
console.error(`\n❌ ERROR: Database file "${dbFile}" is NOT writable!`);
console.error(` Please run on your host: sudo chown 1000:1000 ${dbFile}\n`);
process.exit(1);
}
}
let sqlite: Database.Database;
try {
sqlite = new Database(dbFile);
} catch (err: any) {
if (err.code === 'SQLITE_CANTOPEN') {
console.error(`\n❌ ERROR: SQLite cannot open database file "${dbFile}".`);
console.error(` This is likely a permission issue on the mounted volume.`);
console.error(` Please run on your host: sudo chown -R 1000:1000 ./data\n`);
} else {
console.error(`\n❌ ERROR: Failed to open database:`, err.message);
}
process.exit(1);
}
sqlite.pragma('journal_mode = WAL');
// 4. Initialize Drizzle ORM
+1 -1
View File
@@ -9,7 +9,7 @@ import { WebAuthnService } from '@/features/auth/webAuthnService';
const auth = new Hono<{ Bindings: EnvBindings, Variables: { user: any } }>();
const getService = (c: any) => new AuthService(c.env);
const getWebAuthnService = (c: any) => new WebAuthnService(c.env, c.req.url);
const getWebAuthnService = (c: any) => new WebAuthnService(c.env, c.req.url, c.req.header());
// 获取可用登录方式列表
auth.get('/providers', (c) => {
+9 -3
View File
@@ -15,11 +15,17 @@ export class WebAuthnService {
private rpID: string;
private origin: string;
constructor(env: EnvBindings, url: string) {
constructor(env: EnvBindings, url: string, headers?: Record<string, string | undefined>) {
this.env = env;
const parsedUrl = new URL(url);
this.rpID = parsedUrl.hostname;
this.origin = `${parsedUrl.protocol}//${parsedUrl.host}`;
// Handle Reverse Proxy (Docker/Nginx/Cloudflare)
// If X-Forwarded-Proto exists, use it as the real protocol
const proto = headers?.['x-forwarded-proto'] || parsedUrl.protocol.replace(':', '');
const host = headers?.['x-forwarded-host'] || parsedUrl.host;
this.rpID = host.split(':')[0]; // Domain only for RP ID
this.origin = `${proto.includes('://') ? proto : proto + '://'}${host}`;
}
/**
+3 -3
View File
@@ -25,12 +25,12 @@ services:
# Google 登录配置
# - OAUTH_GOOGLE_CLIENT_ID=
# - OAUTH_GOOGLE_CLIENT_SECRET=
# - OAUTH_GOOGLE_REDIRECT_URI=https://<your_domain>/api/oauth/callback
# - OAUTH_GOOGLE_REDIRECT_URI=https://<your_domain>/oauth/callback
# Github 登录配置
# - OAUTH_GITHUB_CLIENT_ID=
# - OAUTH_GITHUB_CLIENT_SECRET=
# - OAUTH_GITHUB_REDIRECT_URI=https://<your_domain>/api/oauth/callback
# - OAUTH_GITHUB_REDIRECT_URI=https://<your_domain>/oauth/callback
# Telegram 登录配置
# - OAUTH_TELEGRAM_BOT_NAME=your_bot_name
@@ -50,4 +50,4 @@ services:
# Nodeloc 登录配置
# - OAUTH_NODELOC_CLIENT_ID=
# - OAUTH_NODELOC_CLIENT_SECRET=
# - OAUTH_NODELOC_REDIRECT_URI=https://<your_domain>/api/oauth/callback
# - OAUTH_NODELOC_REDIRECT_URI=https://<your_domain>/oauth/callback