chore: add DISABLE_FILESYSTEM_ENCRYPTION option

- Enhanced comments in docker-compose files to clarify the purpose of environment variables, including optional settings for UID, GID, and filesystem encryption.
- Introduced DISABLE_FILESYSTEM_ENCRYPTION variable to allow users to disable file encryption, making the ENCRYPTION_KEY optional.
- Updated documentation in quick-start guide to reflect changes in environment variable usage and security warnings.
This commit is contained in:
Daniel Luiz Alves
2025-07-14 17:25:27 -03:00
parent 90c0300d77
commit 2db88d3902
17 changed files with 314 additions and 81 deletions

View File

@@ -133,6 +133,7 @@ set -e
echo "Starting Palmr Application..."
echo "Storage Mode: \${ENABLE_S3:-false}"
echo "Secure Site: \${SECURE_SITE:-false}"
echo "Encryption: \${DISABLE_FILESYSTEM_ENCRYPTION:-false}"
echo "Database: SQLite"
# Set global environment variables

View File

@@ -1,5 +0,0 @@

> palmr-docs@3.1-beta lint /Users/daniel/clones/Palmr/apps/docs
> eslint "src/**/*.+(ts|tsx)"

View File

@@ -56,6 +56,7 @@ services:
environment:
- ENABLE_S3=false
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY
# - DISABLE_FILESYSTEM_ENCRYPTION=false # Set to true to disable file encryption (ENCRYPTION_KEY becomes optional)
# - SECURE_SITE=false # Set to true if you are using a reverse proxy
ports:
- "5487:5487" # Web interface
@@ -102,9 +103,10 @@ services:
environment:
- ENABLE_S3=false
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY
# - SECURE_SITE=false # Set to true if you are using a reverse proxy
- PALMR_UID=1000 # UID for the container processes (default is 1001)
- PALMR_GID=1000 # GID for the container processes (default is 1001)
# - DISABLE_FILESYSTEM_ENCRYPTION=false # Set to true to disable file encryption (ENCRYPTION_KEY becomes optional)
# - SECURE_SITE=false # Set to true if you are using a reverse proxy
ports:
- "5487:5487" # Web port
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
@@ -128,13 +130,16 @@ docker-compose up -d
Configure Palmr. behavior through environment variables:
| Variable | Default | Description |
| ---------------- | ------- | ------------------------------------------------------- |
| `ENABLE_S3` | `false` | Enable S3-compatible storage |
| `ENCRYPTION_KEY` | - | **Required**: Minimum 32 characters for file encryption |
| `SECURE_SITE` | `false` | Enable secure cookies for HTTPS/reverse proxy setups |
| Variable | Default | Description |
| ------------------------------- | ------- | ------------------------------------------------------------------------------------ |
| `ENABLE_S3` | `false` | Enable S3-compatible storage |
| `ENCRYPTION_KEY` | - | **Required** (unless encryption disabled): Minimum 32 characters for file encryption |
| `DISABLE_FILESYSTEM_ENCRYPTION` | `false` | Disable file encryption for direct filesystem access |
| `SECURE_SITE` | `false` | Enable secure cookies for HTTPS/reverse proxy setups |
> **⚠️ Security Warning**: Always change the `ENCRYPTION_KEY` in production. This key encrypts your files - losing it makes files permanently inaccessible.
> **⚠️ Security Warning**: Always change the `ENCRYPTION_KEY` in production when encryption is enabled. This key encrypts your files - losing it makes files permanently inaccessible.
> **🔓 File Encryption Control**: The `DISABLE_FILESYSTEM_ENCRYPTION` variable allows you to store files without encryption for direct filesystem access. When set to `true`, the `ENCRYPTION_KEY` becomes optional. **Important**: Once set, this configuration is permanent for your deployment. Switching between encrypted and unencrypted modes will break file access for existing uploads. Choose your strategy before uploading files.
> **🔗 Reverse Proxy**: If deploying behind a reverse proxy (Traefik, Nginx, etc.), set `SECURE_SITE=true` and review our [Reverse Proxy Configuration](/docs/3.1-beta/reverse-proxy-configuration) guide for proper setup.
@@ -144,6 +149,8 @@ Need a strong key for `ENCRYPTION_KEY`? Use our built-in generator to create cry
<KeyGenerator />
> **💡 Pro Tip**: If you're using `DISABLE_FILESYSTEM_ENCRYPTION=true`, you can skip the `ENCRYPTION_KEY` entirely for a simpler setup. However, remember that files will be stored unencrypted on your filesystem.
---
## Accessing Palmr.
@@ -177,6 +184,8 @@ docker run -d \
--name palmr \
-e ENABLE_S3=false \
-e ENCRYPTION_KEY=your-secure-key-min-32-chars \
# -e DISABLE_FILESYSTEM_ENCRYPTION=true # Uncomment to disable file encryption (ENCRYPTION_KEY becomes optional)
# -e SECURE_SITE=false # Set to true if you are using a reverse proxy
-p 5487:5487 \
-p 3333:3333 \
-v palmr_data:/app/server \
@@ -184,6 +193,8 @@ docker run -d \
kyantech/palmr:latest
```
> **Permission Configuration**: If you encounter permission issues with bind mounts (common on NAS systems), see our [UID/GID Configuration](/docs/3.1-beta/uid-gid-configuration) guide for automatic permission handling.
**Bind Mount:**
```bash
@@ -191,6 +202,10 @@ docker run -d \
--name palmr \
-e ENABLE_S3=false \
-e ENCRYPTION_KEY=your-secure-key-min-32-chars \
-e PALMR_UID=1000 # UID for the container processes (default is 1001)
-e PALMR_GID=1000 # GID for the container processes (default is 1001)
# -e DISABLE_FILESYSTEM_ENCRYPTION=true # Uncomment to disable file encryption (ENCRYPTION_KEY becomes optional)
# -e SECURE_SITE=false # Set to true if you are using a reverse proxy
-p 5487:5487 \
-p 3333:3333 \
-v $(pwd)/data:/app/server \

View File

@@ -1,5 +0,0 @@

> palmr-api@3.1-beta lint /Users/daniel/clones/Palmr/apps/server
> eslint "src/**/*.+(ts|tsx)"

View File

@@ -24,7 +24,7 @@ export async function buildApp() {
},
},
logger: {
level: "info",
level: "warn",
},
bodyLimit: 1024 * 1024 * 1024 * 1024 * 1024,
connectionTimeout: 0,

View File

@@ -3,6 +3,7 @@ import { z } from "zod";
const envSchema = z.object({
ENABLE_S3: z.union([z.literal("true"), z.literal("false")]).default("false"),
ENCRYPTION_KEY: z.string().optional().default("palmr-default-encryption-key-2025"),
DISABLE_FILESYSTEM_ENCRYPTION: z.union([z.literal("true"), z.literal("false")]).default("false"),
S3_ENDPOINT: z.string().optional(),
S3_PORT: z.string().optional(),
S3_USE_SSL: z.string().optional(),

View File

@@ -92,7 +92,6 @@ export class ChunkManager {
console.log(`Chunk ${chunkIndex} already uploaded, treating as success`);
if (isLastChunk && chunkInfo.uploadedChunks.size === totalChunks) {
// Check if already finalizing to prevent race condition
if (this.finalizingUploads.has(fileId)) {
console.log(`Upload ${fileId} is already being finalized, waiting...`);
return { isComplete: false };
@@ -128,7 +127,6 @@ export class ChunkManager {
);
if (isLastChunk && chunkInfo.uploadedChunks.size === totalChunks) {
// Check if already finalizing to prevent race condition
if (this.finalizingUploads.has(fileId)) {
console.log(`Upload ${fileId} is already being finalized, waiting...`);
return { isComplete: false };
@@ -199,7 +197,7 @@ export class ChunkManager {
}
/**
* Finalize upload by moving temp file to final location and encrypting
* Finalize upload by moving temp file to final location and encrypting (if enabled)
*/
private async finalizeUpload(
chunkInfo: ChunkInfo,
@@ -224,7 +222,7 @@ export class ChunkManager {
const filePath = provider.getFilePath(finalObjectName);
const dir = path.dirname(filePath);
console.log(`Starting encryption and finalization: ${finalObjectName}`);
console.log(`Starting finalization: ${finalObjectName}`);
await fs.promises.mkdir(dir, { recursive: true });
@@ -236,7 +234,6 @@ export class ChunkManager {
});
const encryptStream = provider.createEncryptStream();
// Wait for encryption to complete BEFORE cleaning up temp file
await new Promise<void>((resolve, reject) => {
const startTime = Date.now();
@@ -245,18 +242,17 @@ export class ChunkManager {
.pipe(writeStream)
.on("finish", () => {
const duration = Date.now() - startTime;
console.log(`File encrypted and saved to: ${filePath} in ${duration}ms`);
console.log(`File processed and saved to: ${filePath} in ${duration}ms`);
resolve();
})
.on("error", (error) => {
console.error("Error during encryption:", error);
console.error("Error during processing:", error);
reject(error);
});
});
console.log(`File successfully uploaded and encrypted: ${finalObjectName}`);
console.log(`File successfully uploaded and processed: ${finalObjectName}`);
// Clean up temp file AFTER encryption is complete
await this.cleanupTempFile(chunkInfo.tempPath);
this.activeUploads.delete(chunkInfo.fileId);

View File

@@ -9,6 +9,8 @@ import { ConfigService } from "../config/service";
const execAsync = promisify(exec);
const prisma = new PrismaClient();
// TODO: REMOVE LOGGING AFTER TESTING
export class StorageService {
private configService = new ConfigService();
@@ -21,6 +23,34 @@ export class StorageService {
return Number.isNaN(parsed) ? 0 : parsed;
}
private _parseSize(value: string): number {
if (!value) return 0;
// Remove any whitespace and convert to lowercase
const cleanValue = value.trim().toLowerCase();
// Extract the numeric part
const numericMatch = cleanValue.match(/^(\d+(?:\.\d+)?)/);
if (!numericMatch) return 0;
const numericValue = parseFloat(numericMatch[1]);
if (Number.isNaN(numericValue)) return 0;
// Determine the unit multiplier
if (cleanValue.includes("t")) {
return Math.round(numericValue * 1024 * 1024 * 1024 * 1024); // TB to bytes
} else if (cleanValue.includes("g")) {
return Math.round(numericValue * 1024 * 1024 * 1024); // GB to bytes
} else if (cleanValue.includes("m")) {
return Math.round(numericValue * 1024 * 1024); // MB to bytes
} else if (cleanValue.includes("k")) {
return Math.round(numericValue * 1024); // KB to bytes
} else {
// Assume bytes if no unit
return Math.round(numericValue);
}
}
private async _tryDiskSpaceCommand(command: string): Promise<{ total: number; available: number } | null> {
try {
const { stdout, stderr } = await execAsync(command);
@@ -53,17 +83,79 @@ export class StorageService {
}
}
} else {
// Handle different Linux/Unix command formats
const lines = stdout.trim().split("\n");
if (lines.length >= 2) {
const parts = lines[1].trim().split(/\s+/);
if (parts.length >= 4) {
const [, size, , avail] = parts;
if (command.includes("-B1")) {
total = this._safeParseInt(size);
available = this._safeParseInt(avail);
} else {
total = this._safeParseInt(size) * 1024;
available = this._safeParseInt(avail) * 1024;
// Handle findmnt command output
if (command.includes("findmnt")) {
if (lines.length >= 1) {
const parts = lines[0].trim().split(/\s+/);
if (parts.length >= 2) {
const [availStr, sizeStr] = parts;
available = this._parseSize(availStr);
total = this._parseSize(sizeStr);
}
}
}
// Handle stat -f command output
else if (command.includes("stat -f")) {
// Parse stat -f output (different format)
let blockSize = 0;
let totalBlocks = 0;
let freeBlocks = 0;
for (const line of lines) {
if (line.includes("Block size:")) {
blockSize = this._safeParseInt(line.split(":")[1].trim());
} else if (line.includes("Total blocks:")) {
totalBlocks = this._safeParseInt(line.split(":")[1].trim());
} else if (line.includes("Free blocks:")) {
freeBlocks = this._safeParseInt(line.split(":")[1].trim());
}
}
if (blockSize > 0 && totalBlocks > 0) {
total = totalBlocks * blockSize;
available = freeBlocks * blockSize;
console.log(
`📊 stat -f parsed: blockSize=${blockSize}, totalBlocks=${totalBlocks}, freeBlocks=${freeBlocks}`
);
} else {
console.warn(
`❌ stat -f parsing failed: blockSize=${blockSize}, totalBlocks=${totalBlocks}, freeBlocks=${freeBlocks}`
);
return null;
}
}
// Handle df --output format
else if (command.includes("--output=")) {
if (lines.length >= 2) {
const parts = lines[1].trim().split(/\s+/);
if (parts.length >= 2) {
const [availStr, sizeStr] = parts;
available = this._safeParseInt(availStr) * 1024; // df --output gives in KB
total = this._safeParseInt(sizeStr) * 1024;
}
}
}
// Handle regular df command output
else {
if (lines.length >= 2) {
const parts = lines[1].trim().split(/\s+/);
if (parts.length >= 4) {
const [, size, , avail] = parts;
if (command.includes("-B1")) {
total = this._safeParseInt(size);
available = this._safeParseInt(avail);
} else if (command.includes("-h")) {
// Handle human-readable format (e.g., "1.5G", "500M")
total = this._parseSize(size);
available = this._parseSize(avail);
} else {
// Default to KB (standard df output)
total = this._safeParseInt(size) * 1024;
available = this._safeParseInt(avail) * 1024;
}
}
}
}
@@ -72,7 +164,7 @@ export class StorageService {
if (total > 0 && available >= 0) {
return { total, available };
} else {
console.warn(`Invalid values parsed: total=${total}, available=${available}`);
console.warn(`Invalid values parsed: total=${total}, available=${available} for command: ${command}`);
return null;
}
} catch (error) {
@@ -87,6 +179,7 @@ export class StorageService {
private async _getMountInfo(path: string): Promise<{ filesystem: string; mountPoint: string; type: string } | null> {
try {
if (!fs.existsSync("/proc/mounts")) {
console.log("❌ /proc/mounts not found for mount info");
return null;
}
@@ -96,11 +189,26 @@ export class StorageService {
let bestMatch = null;
let bestMatchLength = 0;
console.log(`🔍 Getting mount info for path: ${path}`);
for (const line of lines) {
const parts = line.split(/\s+/);
if (parts.length >= 3) {
const [filesystem, mountPoint, type] = parts;
// Log interesting filesystems for debugging
if (
filesystem.includes("volume") ||
filesystem.includes("mapper") ||
type === "ext4" ||
type === "btrfs" ||
type === "xfs" ||
mountPoint.includes("volume") ||
mountPoint.includes("app")
) {
console.log(`📋 Mount detail: ${filesystem}${mountPoint} (${type})`);
}
if (path.startsWith(mountPoint) && mountPoint.length > bestMatchLength) {
bestMatch = { filesystem, mountPoint, type };
bestMatchLength = mountPoint.length;
@@ -108,6 +216,10 @@ export class StorageService {
}
}
if (bestMatch) {
console.log(`🎯 Selected mount info: ${bestMatch.filesystem}${bestMatch.mountPoint} (${bestMatch.type})`);
}
return bestMatch;
} catch (error) {
console.warn(`Could not get mount info for ${path}:`, error);
@@ -122,6 +234,7 @@ export class StorageService {
private async _detectMountPoint(path: string): Promise<string | null> {
try {
if (!fs.existsSync("/proc/mounts")) {
console.log("❌ /proc/mounts not found, cannot detect mount points");
return null;
}
@@ -131,22 +244,32 @@ export class StorageService {
let bestMatch = null;
let bestMatchLength = 0;
console.log(`🔍 Checking ${lines.length} mount points for path: ${path}`);
for (const line of lines) {
const parts = line.split(/\s+/);
if (parts.length >= 2) {
const [, mountPoint] = parts;
if (parts.length >= 3) {
const [device, mountPoint, filesystem] = parts;
// Log useful mount information for debugging
if (mountPoint.includes("volume") || mountPoint.includes("app") || mountPoint === "/") {
console.log(`📍 Found mount: ${device}${mountPoint} (${filesystem})`);
}
if (path.startsWith(mountPoint) && mountPoint.length > bestMatchLength) {
bestMatch = mountPoint;
bestMatchLength = mountPoint.length;
console.log(`✅ Better match found: ${mountPoint} (length: ${mountPoint.length})`);
}
}
}
if (bestMatch && bestMatch !== "/") {
console.log(`🎯 Selected mount point: ${bestMatch}`);
return bestMatch;
}
console.log("❌ No specific mount point found, using root");
return null;
} catch (error) {
console.warn(`Could not detect mount point for ${path}:`, error);
@@ -174,11 +297,31 @@ export class StorageService {
? ["wmic logicaldisk get size,freespace,caption"]
: process.platform === "darwin"
? [`df -k "${targetPath}"`, `df "${targetPath}"`]
: [`df -B1 "${targetPath}"`, `df -k "${targetPath}"`, `df "${targetPath}"`];
: [
// Try different df commands for better compatibility
`df -B1 "${targetPath}"`,
`df -k "${targetPath}"`,
`df "${targetPath}"`,
// Additional commands for Synology NAS and other systems
`df -h "${targetPath}"`,
`df -T "${targetPath}"`,
// Fallback to statfs if available
`stat -f "${targetPath}"`,
// Direct filesystem commands
`findmnt -n -o AVAIL,SIZE "${targetPath}"`,
`findmnt -n -o AVAIL,SIZE,TARGET "${targetPath}"`,
// Alternative df with different formatting
`df -P "${targetPath}"`,
`df --output=avail,size "${targetPath}"`,
];
console.log(`🔍 Trying ${commandsToTry.length} commands for path: ${targetPath}`);
for (const command of commandsToTry) {
console.log(`🔧 Executing command: ${command}`);
const result = await this._tryDiskSpaceCommand(command);
if (result) {
console.log(`✅ Command successful: ${command}`);
return {
...result,
mountPoint: mountPoint || undefined,
@@ -186,6 +329,7 @@ export class StorageService {
}
}
console.warn(`❌ All commands failed for path: ${targetPath}`);
return null;
} catch (error) {
console.warn(`Error getting filesystem info for ${path}:`, error);
@@ -193,13 +337,58 @@ export class StorageService {
}
}
/**
* Dynamically detect Synology volume paths by reading /proc/mounts
*/
private async _detectSynologyVolumes(): Promise<string[]> {
try {
if (!fs.existsSync("/proc/mounts")) {
return [];
}
const mountsContent = await fs.promises.readFile("/proc/mounts", "utf8");
const lines = mountsContent.split("\n").filter((line) => line.trim());
const synologyPaths: string[] = [];
for (const line of lines) {
const parts = line.split(/\s+/);
if (parts.length >= 2) {
const [, mountPoint] = parts;
// Check if this is a Synology volume mount point
if (mountPoint.match(/^\/volume\d+$/)) {
synologyPaths.push(mountPoint);
console.log(`🔍 Found Synology volume: ${mountPoint}`);
}
}
}
return synologyPaths;
} catch (error) {
console.warn("Could not detect Synology volumes:", error);
return [];
}
}
private async _getDiskSpaceMultiplePaths(): Promise<{ total: number; available: number } | null> {
const pathsToTry = IS_RUNNING_IN_CONTAINER
? ["/app/server/uploads", "/app/server", "/app", "/"]
// Base paths that work for all systems
const basePaths = IS_RUNNING_IN_CONTAINER
? ["/app/server/uploads", "/app/server/temp-uploads", "/app/server/temp-chunks", "/app/server", "/app", "/"]
: [".", "./uploads", process.cwd()];
// Dynamically detect Synology volume paths
const synologyPaths = await this._detectSynologyVolumes();
// Combine base paths with detected Synology paths
const pathsToTry = [...basePaths, ...synologyPaths];
console.log(`🔍 Attempting disk space detection for ${pathsToTry.length} paths...`);
console.log(`📋 Synology volumes detected: ${synologyPaths.length > 0 ? synologyPaths.join(", ") : "none"}`);
for (const pathToCheck of pathsToTry) {
if (pathToCheck.includes("uploads")) {
console.log(`📁 Checking path: ${pathToCheck}`);
if (pathToCheck.includes("uploads") || pathToCheck.includes("temp-")) {
try {
if (!fs.existsSync(pathToCheck)) {
fs.mkdirSync(pathToCheck, { recursive: true });
@@ -211,6 +400,7 @@ export class StorageService {
}
if (!fs.existsSync(pathToCheck)) {
console.log(`❌ Path does not exist: ${pathToCheck}`);
continue;
}
@@ -220,10 +410,16 @@ export class StorageService {
if (result.mountPoint) {
console.log(`✅ Storage resolved via bind mount: ${result.mountPoint}`);
}
console.log(
`✅ Disk space detected for path ${pathToCheck}: ${(result.total / (1024 * 1024 * 1024)).toFixed(2)}GB total, ${(result.available / (1024 * 1024 * 1024)).toFixed(2)}GB available`
);
return { total: result.total, available: result.available };
} else {
console.log(`❌ No filesystem info available for path: ${pathToCheck}`);
}
}
console.error("❌ All disk space detection attempts failed");
return null;
}

View File

@@ -8,12 +8,12 @@ import { pipeline } from "stream/promises";
import { directoriesConfig, getTempFilePath } from "../config/directories.config";
import { env } from "../env";
import { StorageProvider } from "../types/storage";
import { IS_RUNNING_IN_CONTAINER } from "../utils/container-detection";
export class FilesystemStorageProvider implements StorageProvider {
private static instance: FilesystemStorageProvider;
private uploadsDir: string;
private encryptionKey = env.ENCRYPTION_KEY;
private isEncryptionDisabled = env.DISABLE_FILESYSTEM_ENCRYPTION === "true";
private uploadTokens = new Map<string, { objectName: string; expiresAt: number }>();
private downloadTokens = new Map<string, { objectName: string; expiresAt: number; fileName?: string }>();
@@ -66,6 +66,15 @@ export class FilesystemStorageProvider implements StorageProvider {
}
public createEncryptStream(): Transform {
if (this.isEncryptionDisabled) {
return new Transform({
transform(chunk, encoding, callback) {
this.push(chunk);
callback();
},
});
}
const key = this.createEncryptionKey();
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
@@ -101,6 +110,15 @@ export class FilesystemStorageProvider implements StorageProvider {
}
public createDecryptStream(): Transform {
if (this.isEncryptionDisabled) {
return new Transform({
transform(chunk, encoding, callback) {
this.push(chunk);
callback();
},
});
}
const key = this.createEncryptionKey();
let iv: Buffer | null = null;
let decipher: crypto.Decipher | null = null;
@@ -213,32 +231,26 @@ export class FilesystemStorageProvider implements StorageProvider {
}
}
private encryptFileBuffer(buffer: Buffer): Buffer {
const key = this.createEncryptionKey();
const iv = crypto.randomBytes(16);
const cipher = crypto.createCipheriv("aes-256-cbc", key, iv);
const encrypted = Buffer.concat([iv, cipher.update(buffer), cipher.final()]);
return encrypted;
}
async downloadFile(objectName: string): Promise<Buffer> {
const filePath = this.getFilePath(objectName);
const encryptedBuffer = await fs.readFile(filePath);
const fileBuffer = await fs.readFile(filePath);
if (encryptedBuffer.length > 16) {
if (this.isEncryptionDisabled) {
return fileBuffer;
}
if (fileBuffer.length > 16) {
try {
return this.decryptFileBuffer(encryptedBuffer);
return this.decryptFileBuffer(fileBuffer);
} catch (error: unknown) {
if (error instanceof Error) {
console.warn("Failed to decrypt with new method, trying legacy format", error.message);
}
return this.decryptFileLegacy(encryptedBuffer);
return this.decryptFileLegacy(fileBuffer);
}
}
return this.decryptFileLegacy(encryptedBuffer);
return this.decryptFileLegacy(fileBuffer);
}
private decryptFileBuffer(encryptedBuffer: Buffer): Buffer {

View File

@@ -101,7 +101,9 @@ async function startServer() {
}
console.log(`🌴 Palmr server running on port 3333 🌴`);
console.log(`📦 Storage mode: ${env.ENABLE_S3 === "true" ? "S3" : "Local Filesystem (Encrypted)"}`);
console.log(
`📦 Storage mode: ${env.ENABLE_S3 === "true" ? "S3" : `Local Filesystem ${env.DISABLE_FILESYSTEM_ENCRYPTION === "true" ? "(Unencrypted)" : "(Encrypted)"}`}`
);
console.log(`🔐 Auth Providers: ${authProviders}`);
console.log("\n📚 API Documentation:");

View File

@@ -1,5 +0,0 @@

> palmr-web@3.1-beta lint /Users/daniel/clones/Palmr/apps/web
> eslint "src/**/*.+(ts|tsx)"

View File

@@ -4,10 +4,11 @@ services:
container_name: palmr
environment:
- ENABLE_S3=false
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY
- PALMR_UID=1000 # UID for the container processes (default is 1001) you can change it to the UID of the user running the container
- PALMR_GID=1000 # GID for the container processes (default is 1001) you can change it to the GID of the user running the container
- SECURE_SITE=false # Set to true if you are using a reverse proxy
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY (REQUIRED if DISABLE_FILESYSTEM_ENCRYPTION is false)
- PALMR_UID=1000 # UID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
- PALMR_GID=1000 # GID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
# - SECURE_SITE=true # Set to true if you are using a reverse proxy (OPTIONAL - default is false)
# - DISABLE_FILESYSTEM_ENCRYPTION=true # Set to true to disable file encryption (ENCRYPTION_KEY becomes optional) | (OPTIONAL - default is false)
ports:
- "5487:5487" # Web port
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)

View File

@@ -12,9 +12,9 @@ services:
- S3_REGION=${S3_REGION:-us-east-1} # S3 region (us-east-1 is the default region) but it depends on your s3 server region
- S3_BUCKET_NAME=${S3_BUCKET_NAME:-palmr-files} # Bucket name for the S3 storage (here we are using palmr-files as the bucket name to understand that this is the bucket for palmr)
- S3_FORCE_PATH_STYLE=true # For MinIO compatibility we have to set this to true
- PALMR_UID=1000 # UID for the container processes (default is 1001) you can change it to the UID of the user running the container
- PALMR_GID=1000 # GID for the container processes (default is 1001) you can change it to the GID of the user running the container
- SECURE_SITE=false # Set to true if you are using a reverse proxy
- PALMR_UID=1000 # UID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
- PALMR_GID=1000 # GID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
# - SECURE_SITE=true # Set to true if you are using a reverse proxy (OPTIONAL - default is false)
ports:
- "5487:5487" # Web port
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)

View File

@@ -12,9 +12,9 @@ services:
- S3_REGION=${S3_REGION:-us-east-1} # S3 region (us-east-1 is the default region) but it depends on your s3 server region
- S3_BUCKET_NAME=${S3_BUCKET_NAME:-palmr-files} # Bucket name for the S3 storage (here we are using palmr-files as the bucket name to understand that this is the bucket for palmr)
- S3_FORCE_PATH_STYLE=false # For S3 compatibility we have to set this to false
- PALMR_UID=1000 # UID for the container processes (default is 1001) you can change it to the UID of the user running the container
- PALMR_GID=1000 # GID for the container processes (default is 1001) you can change it to the GID of the user running the container
- SECURE_SITE=false # Set to true if you are using a reverse proxy
- PALMR_UID=1000 # UID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
- PALMR_GID=1000 # GID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
# - SECURE_SITE=true # Set to true if you are using a reverse proxy (OPTIONAL - default is false)
ports:
- "5487:5487" # Web port
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)

View File

@@ -0,0 +1,23 @@
version: "3.8"
services:
palmr:
image: kyantech/palmr:v3.1.1-rc.1
container_name: palmr
environment:
- ENABLE_S3=false
- ENCRYPTION_KEY=palmr-test-encryption-key-2025
- PALMR_UID=1000 # UID for Synology NAS compatibility (default is 1001) | See our UID/GID Documentation for more information
- PALMR_GID=1000 # GID for Synology NAS compatibility (default is 1001) | See our UID/GID Documentation for more information
# - SECURE_SITE=true # Set to true if you are using a reverse proxy (OPTIONAL - default is false)
# - DISABLE_FILESYSTEM_ENCRYPTION=true # Set to true to disable file encryption (ENCRYPTION_KEY becomes optional) | (OPTIONAL - default is false)
# Add any other environment variables as needed
ports:
- "5487:5487"
- "3333:3333"
volumes:
- /volume1/docker/palmr/uploads:/app/server/uploads
- /volume1/docker/palmr/temp-uploads:/app/server/temp-uploads
- /volume1/docker/palmr/temp-chunks:/app/server/temp-chunks
- /volume1/docker/palmr/prisma:/app/server/prisma
restart: unless-stopped

View File

@@ -4,13 +4,14 @@ services:
container_name: palmr
environment:
- ENABLE_S3=false
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY (REQUIRED if DISABLE_FILESYSTEM_ENCRYPTION is false)
- PALMR_UID=1000 # UID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
- PALMR_GID=1000 # GID for the container processes (OPTIONAL - default is 1001) | See our UID/GID Documentation for more information
# - SECURE_SITE=true # Set to true if you are using a reverse proxy (OPTIONAL - default is false)
# - DISABLE_FILESYSTEM_ENCRYPTION=true # Set to true to disable file encryption (ENCRYPTION_KEY becomes optional) | (OPTIONAL - default is false)
ports:
- "5487:5487" # Web port
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
- PALMR_UID=1000 # UID for the container processes (default is 1001) you can change it to the UID of the user running the container
- PALMR_GID=1000 # GID for the container processes (default is 1001) you can change it to the GID of the user running the container
- SECURE_SITE=false # Set to true if you are using a reverse proxy
volumes:
- palmr_data:/app/server # Volume for the application data (changed from /data to /app/server)
restart: unless-stopped # Restart the container unless it is stopped

View File

@@ -20,7 +20,7 @@ environment=PORT=3333,HOME="/home/palmr"
priority=100
[program:web]
command=/bin/sh -c 'echo "Waiting for API to be ready..."; while ! curl -f http://127.0.0.1:3333/health >/dev/null 2>&1; do echo "API not ready, waiting..."; sleep 2; done; echo "API is ready! Starting frontend..."; exec node server.js'
command=/bin/sh -c 'echo "Please wait while Palmr. is starting..."; while ! curl -f http://127.0.0.1:3333/health >/dev/null 2>&1; do echo "1/2 - Palmr. starting, be patient..."; sleep 2; done; echo "2/2 - Palmr. starting, be patient..."; exec node server.js'
directory=/app/web
user=palmr
autostart=true