Load server identity from install dir, not shell CWD.
Release rolling / release (push) Successful in 9m49s
Release rolling / release (push) Successful in 9m49s
Resolve PEARDOCK_HOME / binary path /opt/peardock for .env so manual sudo -u peardock runs no longer hit EACCES on a relative home-dir .env.
This commit is contained in:
@@ -30,6 +30,7 @@ Group=docker
|
||||
SupplementaryGroups=docker
|
||||
|
||||
Environment=NODE_ENV=production
|
||||
Environment=PEARDOCK_HOME=/opt/peardock
|
||||
EnvironmentFile=-/opt/peardock/.env
|
||||
# Installer also writes PEARDOCK_ENGINE / DOCKER_HOST into .env and may set:
|
||||
# Environment=PEARDOCK_ENGINE=podman
|
||||
|
||||
@@ -36,6 +36,22 @@ sudo systemctl status peardock
|
||||
sudo journalctl -u peardock -f
|
||||
```
|
||||
|
||||
The server always loads identity from **`$PEARDOCK_HOME/.env`** (default install: `/opt/peardock/.env`), not from your shell’s current directory. Manual runs:
|
||||
|
||||
```bash
|
||||
# Preferred: match systemd WorkingDirectory
|
||||
cd /opt/peardock && sudo -u peardock ./peardock-server
|
||||
# or:
|
||||
sudo -u peardock env PEARDOCK_HOME=/opt/peardock /opt/peardock/peardock-server
|
||||
```
|
||||
|
||||
If you see `EACCES … open ".env"`, either CWD was wrong (fixed in current builds via exec-path / `/opt/peardock` detection) or ownership is wrong:
|
||||
|
||||
```bash
|
||||
sudo chown peardock:peardock /opt/peardock /opt/peardock/.env
|
||||
sudo chmod 600 /opt/peardock/.env
|
||||
```
|
||||
|
||||
### Debian / Ubuntu notes
|
||||
|
||||
| Engine | Packages / enable | Installer behavior |
|
||||
@@ -52,6 +68,20 @@ sudo journalctl -u peardock -f
|
||||
sudo grep -E '^(PEARDOCK_ENGINE|DOCKER_HOST)=' /opt/peardock/.env
|
||||
```
|
||||
|
||||
Identity is always read/written as **`/opt/peardock/.env`** (or `$PEARDOCK_HOME/.env`), not relative to your shell CWD. Manual run:
|
||||
|
||||
```bash
|
||||
cd /opt/peardock && sudo -u peardock ./peardock-server
|
||||
# or: sudo -u peardock env PEARDOCK_HOME=/opt/peardock /opt/peardock/peardock-server
|
||||
```
|
||||
|
||||
`EACCES … open ".env"` usually means wrong ownership:
|
||||
|
||||
```bash
|
||||
sudo chown -R peardock:peardock /opt/peardock
|
||||
sudo chmod 600 /opt/peardock/.env
|
||||
```
|
||||
|
||||
### Keys: check `/opt/peardock/.env`
|
||||
|
||||
On first successful start the server writes identity into the env file (mode 600):
|
||||
|
||||
@@ -918,6 +918,7 @@ SupplementaryGroups=${eng_grp}
|
||||
NoNewPrivileges=true
|
||||
PrivateTmp=true
|
||||
Environment=NODE_ENV=production
|
||||
Environment=PEARDOCK_HOME=${SERVER_DIR}
|
||||
Environment=PEARDOCK_ENGINE=${eng_kind}
|
||||
Environment=DOCKER_HOST=unix://${eng_sock}
|
||||
Environment=CONTAINER_HOST=unix://${eng_sock}
|
||||
|
||||
+25
-8
@@ -10,16 +10,22 @@ import b4a from 'b4a'
|
||||
import crypto from 'hypercore-crypto'
|
||||
import dotenv from 'dotenv'
|
||||
import logger from '../utils/logger.js'
|
||||
import { ensureServerDataDir, resolveEnvFile } from './paths.js'
|
||||
|
||||
dotenv.config()
|
||||
const { dataDir, envFile: defaultEnvFile } = ensureServerDataDir()
|
||||
|
||||
// Load .env from install/data dir (not process CWD) so manual runs work.
|
||||
dotenv.config({ path: defaultEnvFile })
|
||||
|
||||
const log = logger.child('keys')
|
||||
log.debug('Server data directory', { dataDir, envFile: defaultEnvFile })
|
||||
|
||||
/**
|
||||
* @param {string} [envPath]
|
||||
* @returns {{ seed: Uint8Array, keyPair: { publicKey: Uint8Array, secretKey: Uint8Array }, publicKeyHex: string, seedHex: string }}
|
||||
*/
|
||||
export function loadOrCreateKeyPair(envPath = '.env') {
|
||||
export function loadOrCreateKeyPair(envPath = resolveEnvFile()) {
|
||||
const resolved = path.resolve(envPath)
|
||||
let seedHex = process.env.SERVER_SEED || process.env.SERVER_KEY
|
||||
|
||||
// SERVER_KEY historically was a 32-byte topic seed; reuse as DHT seed if present
|
||||
@@ -28,9 +34,18 @@ export function loadOrCreateKeyPair(envPath = '.env') {
|
||||
seedHex = b4a.toString(seed, 'hex')
|
||||
const publicKeyHex = b4a.toString(DHT.keyPair(seed).publicKey, 'hex')
|
||||
const line = `\nSERVER_SEED=${seedHex}\nSERVER_PUBLIC_KEY=${publicKeyHex}\n`
|
||||
fs.appendFileSync(envPath, line, { flag: 'a' })
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(resolved), { recursive: true })
|
||||
fs.appendFileSync(resolved, line, { flag: 'a', mode: 0o600 })
|
||||
} catch (err) {
|
||||
const hint =
|
||||
err?.code === 'EACCES'
|
||||
? ` Permission denied writing ${resolved}. Run with WorkingDirectory set to the install dir (systemd) or: sudo chown peardock:peardock ${path.dirname(resolved)} ${resolved}`
|
||||
: ''
|
||||
throw new Error(`Cannot write identity file ${resolved}: ${err.message}.${hint}`)
|
||||
}
|
||||
log.info('Generated new SERVER_SEED and SERVER_PUBLIC_KEY', {
|
||||
path: path.resolve(envPath),
|
||||
path: resolved,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -45,21 +60,23 @@ export function loadOrCreateKeyPair(envPath = '.env') {
|
||||
// Keep PUBLIC_KEY in env for operator convenience
|
||||
if (process.env.SERVER_PUBLIC_KEY !== publicKeyHex) {
|
||||
try {
|
||||
let env = fs.existsSync(envPath) ? fs.readFileSync(envPath, 'utf8') : ''
|
||||
let env = fs.existsSync(resolved) ? fs.readFileSync(resolved, 'utf8') : ''
|
||||
if (env.includes('SERVER_PUBLIC_KEY=')) {
|
||||
env = env.replace(/SERVER_PUBLIC_KEY=.*/g, `SERVER_PUBLIC_KEY=${publicKeyHex}`)
|
||||
} else {
|
||||
env += `\nSERVER_PUBLIC_KEY=${publicKeyHex}\n`
|
||||
}
|
||||
if (!env.includes('SERVER_SEED=') && !process.env.SERVER_SEED) {
|
||||
// migrate old SERVER_KEY-only files
|
||||
if (!env.includes('SERVER_SEED=')) {
|
||||
env += `SERVER_SEED=${seedHex}\n`
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(envPath, env)
|
||||
fs.writeFileSync(resolved, env, { mode: 0o600 })
|
||||
} catch (err) {
|
||||
log.warn('Could not update .env with SERVER_PUBLIC_KEY', { error: err.message })
|
||||
log.warn('Could not update .env with SERVER_PUBLIC_KEY', {
|
||||
error: err.message,
|
||||
path: resolved,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
/**
|
||||
* Resolve peardock server data directory and .env path.
|
||||
*
|
||||
* Manual runs often start from a home directory (`sudo -u peardock /opt/peardock/peardock-server`)
|
||||
* while systemd sets WorkingDirectory=/opt/peardock. Relative `.env` must not depend on CWD.
|
||||
*
|
||||
* Priority:
|
||||
* 1. PEARDOCK_HOME / PEARDOCK_SERVER_DIR
|
||||
* 2. Directory of PEARDOCK_ENV_PATH
|
||||
* 3. Directory of the peardock-server binary (process.execPath)
|
||||
* 4. /opt/peardock when it looks like an install tree
|
||||
* 5. Package root (dev checkout)
|
||||
* 6. process.cwd()
|
||||
*/
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
/**
|
||||
* @param {string} dir
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function looksLikeInstallDir(dir) {
|
||||
if (!dir) return false
|
||||
try {
|
||||
if (fs.existsSync(path.join(dir, '.env'))) return true
|
||||
if (fs.existsSync(path.join(dir, 'peardock-server'))) return true
|
||||
if (fs.existsSync(path.join(dir, 'package.json'))) {
|
||||
// Prefer package root only when it is clearly peardock
|
||||
try {
|
||||
const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'))
|
||||
if (pkg?.name === 'peardock') return true
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} absolute data directory
|
||||
*/
|
||||
export function resolveServerDataDir() {
|
||||
const fromEnv =
|
||||
process.env.PEARDOCK_HOME ||
|
||||
process.env.PEARDOCK_SERVER_DIR ||
|
||||
process.env.PEARDOCK_DATA_DIR
|
||||
if (fromEnv && String(fromEnv).trim()) {
|
||||
return path.resolve(String(fromEnv).trim())
|
||||
}
|
||||
|
||||
if (process.env.PEARDOCK_ENV_PATH) {
|
||||
return path.dirname(path.resolve(process.env.PEARDOCK_ENV_PATH))
|
||||
}
|
||||
|
||||
// Standalone binary: /opt/peardock/peardock-server
|
||||
try {
|
||||
const execPath = process.execPath || ''
|
||||
const base = path.basename(execPath).toLowerCase()
|
||||
if (base.includes('peardock-server') || base === 'peardock') {
|
||||
const dir = path.dirname(path.resolve(execPath))
|
||||
if (looksLikeInstallDir(dir) || dir === '/opt/peardock') return dir
|
||||
// Bare often embeds; still prefer exec dir when binary name matches
|
||||
if (dir && dir !== '/' && !dir.includes('node_modules')) return dir
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
if (looksLikeInstallDir('/opt/peardock')) {
|
||||
return '/opt/peardock'
|
||||
}
|
||||
|
||||
// Dev: server/core/paths.js → repo root
|
||||
try {
|
||||
const here = path.dirname(fileURLToPath(import.meta.url))
|
||||
const root = path.resolve(here, '../..')
|
||||
if (looksLikeInstallDir(root)) return root
|
||||
} catch {
|
||||
// Bare may not support import.meta.url the same way
|
||||
}
|
||||
|
||||
return path.resolve(process.cwd())
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute path to the identity / env file.
|
||||
* @returns {string}
|
||||
*/
|
||||
export function resolveEnvFile() {
|
||||
if (process.env.PEARDOCK_ENV_PATH && String(process.env.PEARDOCK_ENV_PATH).trim()) {
|
||||
return path.resolve(String(process.env.PEARDOCK_ENV_PATH).trim())
|
||||
}
|
||||
return path.join(resolveServerDataDir(), '.env')
|
||||
}
|
||||
|
||||
/**
|
||||
* Pin PEARDOCK_HOME and prefer chdir into the data dir so relative state files land correctly.
|
||||
* Safe to call multiple times.
|
||||
* @returns {{ dataDir: string, envFile: string }}
|
||||
*/
|
||||
export function ensureServerDataDir() {
|
||||
const dataDir = resolveServerDataDir()
|
||||
const envFile = resolveEnvFile()
|
||||
|
||||
if (!process.env.PEARDOCK_HOME) {
|
||||
process.env.PEARDOCK_HOME = dataDir
|
||||
}
|
||||
|
||||
try {
|
||||
if (path.resolve(process.cwd()) !== dataDir) {
|
||||
process.chdir(dataDir)
|
||||
}
|
||||
} catch {
|
||||
// Non-fatal: still use absolute env path for keys
|
||||
}
|
||||
|
||||
return { dataDir, envFile }
|
||||
}
|
||||
+10
-1
@@ -7,8 +7,12 @@
|
||||
import DHT from 'hyperdht'
|
||||
import b4a from 'b4a'
|
||||
import gracefulGoodbye from 'graceful-goodbye'
|
||||
import { ensureServerDataDir } from './core/paths.js'
|
||||
import { loadOrCreateKeyPair } from './core/keys.js'
|
||||
import { initAuthKeys } from './core/auth-keys.js'
|
||||
|
||||
// Pin CWD / PEARDOCK_HOME before other side-effectful services use relative paths
|
||||
const { dataDir: serverDataDir, envFile: serverEnvFile } = ensureServerDataDir()
|
||||
import { peers } from './core/peer-registry.js'
|
||||
import { PeerSession } from './rpc/session.js'
|
||||
import { registerAllHandlers, cleanupSession } from './rpc/register.js'
|
||||
@@ -45,8 +49,13 @@ let seedHex
|
||||
try {
|
||||
;({ keyPair, publicKeyHex, seedHex } = loadOrCreateKeyPair())
|
||||
initAuthKeys({ seedHex, publicKeyHex })
|
||||
log.info('Server identity ready', { dataDir: serverDataDir, envFile: serverEnvFile })
|
||||
} catch (err) {
|
||||
log.error('Failed to load server keypair', { error: err.message })
|
||||
log.error('Failed to load server keypair', {
|
||||
error: err.message,
|
||||
dataDir: serverDataDir,
|
||||
envFile: serverEnvFile,
|
||||
})
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import test from 'brittle'
|
||||
import path from 'path'
|
||||
import fs from 'fs'
|
||||
import os from 'os'
|
||||
import {
|
||||
resolveServerDataDir,
|
||||
resolveEnvFile,
|
||||
ensureServerDataDir,
|
||||
} from '../server/core/paths.js'
|
||||
|
||||
test('resolveServerDataDir respects PEARDOCK_HOME', (t) => {
|
||||
const prev = process.env.PEARDOCK_HOME
|
||||
process.env.PEARDOCK_HOME = '/opt/peardock'
|
||||
t.is(resolveServerDataDir(), path.resolve('/opt/peardock'))
|
||||
if (prev === undefined) delete process.env.PEARDOCK_HOME
|
||||
else process.env.PEARDOCK_HOME = prev
|
||||
})
|
||||
|
||||
test('resolveEnvFile joins data dir', (t) => {
|
||||
const prevH = process.env.PEARDOCK_HOME
|
||||
const prevE = process.env.PEARDOCK_ENV_PATH
|
||||
delete process.env.PEARDOCK_ENV_PATH
|
||||
process.env.PEARDOCK_HOME = '/opt/peardock'
|
||||
t.is(resolveEnvFile(), path.join(path.resolve('/opt/peardock'), '.env'))
|
||||
if (prevH === undefined) delete process.env.PEARDOCK_HOME
|
||||
else process.env.PEARDOCK_HOME = prevH
|
||||
if (prevE === undefined) delete process.env.PEARDOCK_ENV_PATH
|
||||
else process.env.PEARDOCK_ENV_PATH = prevE
|
||||
})
|
||||
|
||||
test('resolveEnvFile respects PEARDOCK_ENV_PATH', (t) => {
|
||||
const prev = process.env.PEARDOCK_ENV_PATH
|
||||
process.env.PEARDOCK_ENV_PATH = '/custom/identity.env'
|
||||
t.is(resolveEnvFile(), path.resolve('/custom/identity.env'))
|
||||
if (prev === undefined) delete process.env.PEARDOCK_ENV_PATH
|
||||
else process.env.PEARDOCK_ENV_PATH = prev
|
||||
})
|
||||
|
||||
test('ensureServerDataDir sets PEARDOCK_HOME', (t) => {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'peardock-paths-'))
|
||||
const prevH = process.env.PEARDOCK_HOME
|
||||
const prevCwd = process.cwd()
|
||||
try {
|
||||
process.env.PEARDOCK_HOME = dir
|
||||
const r = ensureServerDataDir()
|
||||
t.is(r.dataDir, path.resolve(dir))
|
||||
t.is(r.envFile, path.join(path.resolve(dir), '.env'))
|
||||
t.is(process.env.PEARDOCK_HOME, path.resolve(dir))
|
||||
} finally {
|
||||
try {
|
||||
process.chdir(prevCwd)
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
if (prevH === undefined) delete process.env.PEARDOCK_HOME
|
||||
else process.env.PEARDOCK_HOME = prevH
|
||||
try {
|
||||
fs.rmSync(dir, { recursive: true, force: true })
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user