Compare commits

...

33 Commits

Author SHA1 Message Date
Daniel Luiz Alves
cd14c28be1 refactor: simplify Docker environment detection for file storage paths (#77) 2025-06-19 03:02:31 -03:00
Daniel Luiz Alves
3c084a6686 refactor: simplify Docker environment detection for file storage paths
- Replaced manual Docker detection logic with a utility constant for determining if the application is running in a container.
- Updated file storage paths in both server and filesystem storage provider to use the new constant for improved readability and maintainability.
2025-06-19 02:49:47 -03:00
Daniel Luiz Alves
6a1381684b refactor: replace FilePreviewModal with ShareFilePreviewModal (#76) 2025-06-19 02:01:07 -03:00
Daniel Luiz Alves
dc20770fe6 refactor: replace FilePreviewModal with ShareFilePreviewModal in files table component
- Updated the files table component to use ShareFilePreviewModal for file previews.
- Removed the unused import of FilePreviewModal and added the new import for ShareFilePreviewModal.
2025-06-19 01:46:50 -03:00
Daniel Luiz Alves
6e526f7f88 fix: update email transport secure (#75) 2025-06-19 00:51:27 -03:00
Daniel Luiz Alves
858852c8cd refactor: remove unused import from email service 2025-06-19 00:50:09 -03:00
Daniel Luiz Alves
363dedbb2c Update service.ts (#74) 2025-06-19 00:49:27 -03:00
TerrifiedBug
cd215c79b8 Update service.ts
Fix nodemailer secure flag for STARTTLS
2025-06-18 23:45:42 +01:00
Daniel Luiz Alves
98586efbcd v3.0.0-beta.5 (#72) 2025-06-18 18:31:09 -03:00
Daniel Luiz Alves
c724e644c7 fix: update notification endpoint and include request body in API call
- Changed the API endpoint for notifying recipients to include the shareId directly in the URL.
- Added the request body to the fetch call to ensure proper data is sent with the notification request.
- Set the Content-Type header to application/json for the request.
2025-06-18 18:14:20 -03:00
Daniel Luiz Alves
555ff18a87 feat: implement Docker compatibility for file storage paths (#71) 2025-06-18 18:06:51 -03:00
Daniel Luiz Alves
5100e1591b feat: implement Docker compatibility for file storage paths
- Added checks to determine if the application is running in a Docker environment.
- Updated file storage paths to use `/app/server` in Docker and the current working directory for local development.
- Ensured consistent directory creation for uploads and temporary chunks across different environments.
2025-06-18 18:05:46 -03:00
Daniel Luiz Alves
6de29bbf07 fix: standardize environment variable imports and enhance user auth (#69) 2025-06-18 17:08:59 -03:00
Daniel Luiz Alves
39c47be940 fix: standardize environment variable imports and enhance user authentication error handling
- Updated imports for environment variables in auth and email services to ensure consistency.
- Improved error handling in user routes to provide more specific responses for unauthorized access and internal server errors.
2025-06-18 16:57:11 -03:00
Daniel Luiz Alves
76d96816bc v3.0.0-beta.3 (#68) 2025-06-18 16:19:24 -03:00
Daniel Luiz Alves
b3e7658a76 feat: enhance authentication flow and improve database setup script (#67) 2025-06-18 15:32:41 -03:00
Daniel Luiz Alves
61a579aeb3 feat: enhance authentication flow and improve database setup script
- Added a check for first user access in the authentication context to handle initial user setup.
- Updated the server start script to ensure proper ownership and permissions for database operations, enhancing compatibility with Docker environments.
- Refactored database seeding and configuration checks to run as the target user, preventing permission issues during setup.
2025-06-18 15:32:12 -03:00
Daniel Luiz Alves
cc9c375774 feat: add reverse proxy support (#66) 2025-06-18 12:44:39 -03:00
Daniel Luiz Alves
016006ba3d fix: storage calculation when running within docker (#65) 2025-06-18 12:44:20 -03:00
ruohki
cbc567c6a8 fixed logic error 2025-06-18 17:26:23 +02:00
Daniel Luiz Alves
25b4d886f7 docs: Update reverse proxy configuration to address SQLite "readonly database" error
- Added guidance for configuring proper UID/GID permissions to resolve SQLite issues with bind mounts.
- Included a note on checking host UID/GID and linked to detailed setup documentation for clarity.
2025-06-18 12:23:00 -03:00
ruohki
98953e042b check if runs within docker to pick storage loc 2025-06-18 17:16:11 +02:00
Daniel Luiz Alves
9e06a67593 docs: remove outdated Nginx configuration from reverse proxy documentation
- Eliminated the Nginx HTTP configuration section for reverse proxies without HTTPS/SSL to streamline the documentation.
- Maintained focus on the SECURE_SITE variable and Docker Compose setup for clarity in reverse proxy configurations.
2025-06-18 12:14:56 -03:00
Daniel Luiz Alves
9682f96905 docs: reverse proxy documentation to streamline Docker Compose example
- Removed outdated Docker Compose configuration for the Palmr service.
- Retained the SECURE_SITE environment variable setting for clarity.
- Updated documentation to emphasize HTTP security considerations.
2025-06-18 12:14:05 -03:00
Daniel Luiz Alves
d2c69c3b36 feat: Add SECURE_SITE configuration and reverse proxy documentation
- Introduced the SECURE_SITE environment variable to control cookie security settings based on deployment context.
- Updated Dockerfile to log SECURE_SITE status during application startup.
- Enhanced documentation with a new guide on reverse proxy configuration, detailing the use of SECURE_SITE for secure cookie handling.
- Adjusted authentication and email services to utilize SECURE_SITE for secure connections.
- Updated frontend components to set cookie security based on the current protocol.
2025-06-18 12:10:54 -03:00
Daniel Luiz Alves
9afe8292fa v3.0.0-beta.2 (#62) 2025-06-17 23:43:12 -03:00
Daniel Luiz Alves
e15f50a8a8 fix(docker): Implement bind mount compose (#61) 2025-06-17 23:31:06 -03:00
Daniel Luiz Alves
8affdc8f95 (fix) Share download (#58) 2025-06-17 23:27:38 -03:00
Daniel Luiz Alves
281eff0f14 chore: update Dockerfile and supervisord configuration for improved logging
- Modified the Dockerfile to streamline the creation of the supervisor configuration directory.
- Updated `infra/supervisord.conf` to redirect logs to stdout and stderr, enhancing log management and visibility.
- Removed specific log file paths and sizes to simplify logging setup.
2025-06-17 23:23:36 -03:00
Daniel Luiz Alves
b28f1f97c4 Refactor Dockerfile to use external supervisord configuration file
- Replaced inline supervisor configuration in Dockerfile with a separate `infra/supervisord.conf` file for better organization and maintainability.
- Ensured the new configuration retains all previous settings for the server and web programs.
2025-06-17 23:14:40 -03:00
Daniel Luiz Alves
c5660b3c6b feat: Enhance Docker setup and documentation for Palmr.
- Added a new `docker-compose-bind-mount-example.yaml` for easier bind mount configuration.
- Updated `.gitignore` to include the `data/` directory for persistent storage.
- Modified `docker-compose.yaml` to clarify volume paths and improve comments.
- Enhanced `Dockerfile` to support flexible UID/GID configuration and ensure proper directory permissions.
- Updated environment variable handling in `server-start.sh` and Prisma configuration for better database management.
- Revised documentation in `quick-start.mdx` and `uid-gid-configuration.mdx` to reflect new features and best practices for deployment.
2025-06-17 22:46:28 -03:00
Charly Gley
e64f718998 fix: change error and success messages 2025-06-17 19:59:27 +02:00
Charly Gley
f00a9dadd0 fix: make download on shares work 2025-06-17 19:55:23 +02:00
28 changed files with 1187 additions and 384 deletions

3
.gitignore vendored
View File

@@ -29,4 +29,5 @@ apps/server/.env
apps/server/dist/*
#DEFAULT
.env
.env
data/

View File

@@ -1,10 +1,11 @@
FROM node:20-alpine AS base
# Install system dependencies (removed netcat-openbsd since we no longer need to wait for PostgreSQL)
# Install system dependencies
RUN apk add --no-cache \
gcompat \
supervisor \
curl
curl \
su-exec
# Enable pnpm
RUN corepack enable pnpm
@@ -80,15 +81,12 @@ ARG PALMR_GID=1001
RUN addgroup --system --gid ${PALMR_GID} nodejs
RUN adduser --system --uid ${PALMR_UID} --ingroup nodejs palmr
# Create application directories and set permissions
# Include storage directories for filesystem mode and SQLite database directory
RUN mkdir -p /app/server /app/web /home/palmr/.npm /home/palmr/.cache \
/app/server/uploads /app/server/temp-chunks /app/server/uploads/logo \
/app/server/prisma
# Create application directories
RUN mkdir -p /app/palmr-app /app/web /home/palmr/.npm /home/palmr/.cache
RUN chown -R palmr:nodejs /app /home/palmr
# === Copy Server Files ===
WORKDIR /app/server
# === Copy Server Files to /app/palmr-app (separate from /app/server for bind mounts) ===
WORKDIR /app/palmr-app
# Copy server production files
COPY --from=server-builder --chown=palmr:nodejs /app/server/dist ./dist
@@ -101,8 +99,9 @@ COPY --from=server-builder --chown=palmr:nodejs /app/server/reset-password.sh ./
COPY --from=server-builder --chown=palmr:nodejs /app/server/src/scripts/ ./src/scripts/
RUN chmod +x ./reset-password.sh
# Ensure storage directories have correct permissions
RUN chown -R palmr:nodejs /app/server/uploads /app/server/temp-chunks /app/server/prisma
# Copy seed file to the shared location for bind mounts
RUN mkdir -p /app/server/prisma
COPY --from=server-builder --chown=palmr:nodejs /app/server/prisma/seed.js /app/server/prisma/seed.js
# === Copy Web Files ===
WORKDIR /app/web
@@ -116,104 +115,35 @@ COPY --from=web-builder --chown=palmr:nodejs /app/web/.next/static ./.next/stati
WORKDIR /app
# Create supervisor configuration
RUN mkdir -p /etc/supervisor/conf.d /var/log/supervisor
RUN mkdir -p /etc/supervisor/conf.d
# Copy server start script
COPY infra/server-start.sh /app/server-start.sh
RUN chmod +x /app/server-start.sh
RUN chown palmr:nodejs /app/server-start.sh
# Copy supervisor configuration (simplified without PostgreSQL dependency)
COPY <<EOF /etc/supervisor/conf.d/supervisord.conf
[supervisord]
nodaemon=true
user=root
logfile=/var/log/supervisor/supervisord.log
pidfile=/var/run/supervisord.pid
# Copy supervisor configuration
COPY infra/supervisord.conf /etc/supervisor/conf.d/supervisord.conf
[program:server]
command=/app/server-start.sh
directory=/app/server
user=palmr
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/server.err.log
stdout_logfile=/var/log/supervisor/server.out.log
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'
directory=/app/web
user=palmr
autostart=true
autorestart=true
stderr_logfile=/var/log/supervisor/web.err.log
stdout_logfile=/var/log/supervisor/web.out.log
environment=PORT=5487,HOSTNAME="0.0.0.0",HOME="/home/palmr",API_BASE_URL="http://127.0.0.1:3333"
priority=200
startsecs=10
EOF
# Create main startup script with UID/GID runtime support
# Create main startup script
COPY <<EOF /app/start.sh
#!/bin/sh
set -e
echo "Starting Palmr Application..."
echo "Storage Mode: \${ENABLE_S3:-false}"
echo "Secure Site: \${SECURE_SITE:-false}"
echo "Database: SQLite"
# Runtime UID/GID configuration - only apply if environment variables are set
if [ -n "\${PALMR_UID}" ] || [ -n "\${PALMR_GID}" ]; then
RUNTIME_UID=\${PALMR_UID:-${PALMR_UID}}
RUNTIME_GID=\${PALMR_GID:-${PALMR_GID}}
echo "Runtime UID/GID configuration detected: UID=\$RUNTIME_UID, GID=\$RUNTIME_GID"
# Get current user/group IDs
CURRENT_UID=\$(id -u palmr 2>/dev/null || echo "${PALMR_UID}")
CURRENT_GID=\$(id -g palmr 2>/dev/null || echo "${PALMR_GID}")
# Only modify if different from current
if [ "\$CURRENT_UID" != "\$RUNTIME_UID" ] || [ "\$CURRENT_GID" != "\$RUNTIME_GID" ]; then
echo "Adjusting user/group IDs from \$CURRENT_UID:\$CURRENT_GID to \$RUNTIME_UID:\$RUNTIME_GID"
# Modify group if needed
if [ "\$CURRENT_GID" != "\$RUNTIME_GID" ]; then
if getent group \$RUNTIME_GID >/dev/null 2>&1; then
EXISTING_GROUP=\$(getent group \$RUNTIME_GID | cut -d: -f1)
echo "Using existing group with GID \$RUNTIME_GID: \$EXISTING_GROUP"
usermod -g \$EXISTING_GROUP palmr 2>/dev/null || echo "Warning: Could not change user group"
else
groupmod -g \$RUNTIME_GID nodejs 2>/dev/null || echo "Warning: Could not modify group GID"
fi
fi
# Modify user if needed
if [ "\$CURRENT_UID" != "\$RUNTIME_UID" ]; then
if getent passwd \$RUNTIME_UID >/dev/null 2>&1; then
EXISTING_USER=\$(getent passwd \$RUNTIME_UID | cut -d: -f1)
echo "Warning: UID \$RUNTIME_UID already exists as user '\$EXISTING_USER'"
echo "Container will continue but may have permission issues"
else
usermod -u \$RUNTIME_UID palmr 2>/dev/null || echo "Warning: Could not modify user UID"
fi
fi
# Update file ownership for application directories
echo "Updating file ownership for application directories..."
chown -R palmr:nodejs /app /home/palmr 2>/dev/null || echo "Warning: Could not update all file ownership"
else
echo "Runtime UID/GID matches current values, no changes needed"
fi
else
echo "No runtime UID/GID configuration provided, using defaults"
fi
# Set global environment variables
export DATABASE_URL="file:/app/server/prisma/palmr.db"
export UPLOAD_PATH="/app/server/uploads"
export TEMP_CHUNKS_PATH="/app/server/temp-chunks"
# Ensure storage directories exist with correct permissions
# Ensure /app/server directory exists for bind mounts
mkdir -p /app/server/uploads /app/server/temp-chunks /app/server/uploads/logo /app/server/prisma
chown -R palmr:nodejs /app/server/uploads /app/server/temp-chunks /app/server/prisma 2>/dev/null || echo "Warning: Could not set permissions on storage directories"
echo "Data directories ready for first run..."
# Start supervisor
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf
@@ -221,8 +151,8 @@ EOF
RUN chmod +x /app/start.sh
# Create volume mount points for persistent storage (filesystem mode and SQLite database)
VOLUME ["/app/server/uploads", "/app/server/temp-chunks", "/app/server/prisma"]
# Create volume mount points for bind mounts
VOLUME ["/app/server"]
# Expose ports
EXPOSE 3333 5487
@@ -232,4 +162,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:5487 || exit 1
# Start application
CMD ["/app/start.sh"]
CMD ["/app/start.sh"]

View File

@@ -15,6 +15,7 @@
"configuring-smtp",
"available-languages",
"uid-gid-configuration",
"reverse-proxy-configuration",
"password-reset-without-smtp",
"oidc-authentication",
"---Developers---",

View File

@@ -3,32 +3,50 @@ title: Quick Start (Docker)
icon: "Rocket"
---
Hey there! Welcome to the fastest way to launch <span className="font-bold">Palmr.</span>, your very own secure <span className="font-bold italic">file sharing solution</span>. Whether you're a first-timer to <span className="font-bold italic">self-hosting</span> or a tech wizard, we've made this process incredibly straightforward. In just a few minutes, you'll have a sleek, user-friendly <span className="font-bold italic">file sharing platform</span> running on your <span className="font-bold italic">server</span> or <span className="font-bold italic">VPS</span>.
Welcome to the fastest way to deploy <span className="font-bold">Palmr.</span> - your secure, self-hosted file sharing solution. This guide will have you up and running in minutes, whether you're new to self-hosting or an experienced developer.
This guide is all about speed and simplicity, using our built-in <span className="font-bold italic">file storage system</span> ideal for most users. While Palmr. supports advanced setups like <span className="font-bold italic">manual installation</span> or <span className="font-bold italic">Amazon S3-compatible external storage</span>, we're focusing on the easiest path with <span className="font-bold italic">Docker Compose</span>. Curious about other options? Check out the dedicated sections in our docs for those advanced configurations.
Palmr. offers flexible deployment options to match your infrastructure needs. This guide focuses on Docker deployment with our recommended filesystem storage, perfect for most use cases.
Let's dive in and get Palmr. up and running!
## Prerequisites
## What you'll need
Ensure you have the following installed on your system:
To get started, you only need two tools installed on your system. Don't worry, they're easy to set up:
- **Docker** - Container runtime ([installation guide](https://docs.docker.com/get-docker/))
- **Docker Compose** - Multi-container orchestration ([installation guide](https://docs.docker.com/compose/install/))
- **Docker** ([https://docs.docker.com](https://docs.docker.com/)) - This will run Palmr. in a container.
- **Docker Compose** ([https://docs.docker.com/compose](https://docs.docker.com/compose/)) - This helps manage the setup with a simple configuration file.
> **Platform Support**: Palmr. is developed on macOS and extensively tested on Linux servers. While we haven't formally tested other platforms, Docker's cross-platform nature should ensure compatibility. Report any issues on our [GitHub repository](https://github.com/kyantech/Palmr/issues).
> **Note**: Palmr. was developed on **MacOS** and thoroughly tested on **Linux servers**, ensuring top-notch performance on these platforms. We haven't tested on **Windows** or other environments yet, so there might be some hiccups. Since we're still in **beta**, bugs can pop up anywhere. If you spot an issue, we'd love your help please report it on our GitHub [issues page](https://github.com/kyantech/Palmr/issues).
## Storage Options
## Setting up with Docker Compose
Palmr. supports two storage approaches for persistent data:
Docker Compose is the simplest way to deploy Palmr. across different environments. Once you've got Docker and Docker Compose installed, you're ready to roll with our streamlined setup.
### Named Volumes (Recommended)
In the root folder of the Palmr. project, you'll find a few compose files. For this guide, we're using `docker-compose.yaml` the only file you need to run Palmr. with file system storage. No need to build anything yourself; our pre-built images are hosted on [DockerHub](https://hub.docker.com/repositories/kyantech) and referenced in this file.
**Best for**: Production environments, automated deployments
You can tweak settings directly in `docker-compose.yaml` or use environment variables (more on that later). Let's take a closer look at what's inside this file.
- ✅ **Managed by Docker**: No permission issues or manual path management
- ✅ **Optimized Performance**: Docker-native storage optimization
- ✅ **Cross-platform**: Consistent behavior across operating systems
- ✅ **Simplified Backups**: Docker volume commands for backup/restore
## Exploring the docker-compose.yaml file
### Bind Mounts
Here's the full content of our `docker-compose.yaml`. Feel free to copy it from here or grab it from our official repository ([Docker Compose](https://github.com/kyantech/Palmr/blob/main/docker-compose.yaml)).
**Best for**: Development, direct file access requirements
- ✅ **Direct Access**: Files stored in local directory you specify
- ✅ **Transparent Storage**: Direct filesystem access from host
- ✅ **Custom Backup**: Use existing file system backup solutions
- ⚠️ **Permission Considerations**: May require user/group configuration
---
## Option 1: Named Volumes (Recommended)
Named volumes provide the best performance and are managed entirely by Docker.
### Configuration
Use the provided `docker-compose.yaml` for named volumes:
```yaml
services:
@@ -38,104 +56,118 @@ 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
ports:
- "5487:5487" # Web port
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY, IF YOU DONT WANT TO EXPOSE THE API, JUST REMOVE THIS LINE )
- "5487:5487" # Web interface
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
volumes:
- palmr_data:/app/server # Volume for the application data
- palmr_data:/app/server # Named volume for the application data
restart: unless-stopped # Restart the container unless it is stopped
volumes:
palmr_data:
```
We've added helpful comments in the file to guide you through customization. Let's break down what you can adjust to fit your setup.
---
### Understanding the services
Palmr. runs as a single service in this filesystem storage setup. Here's a quick overview:
| **Service** | **Image** | **Exposed Ports** | **Main Features** |
| ----------- | ---------------------------------------------------------------------------------------- | --------------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| palmr | [kyantech/palmr:latest](https://hub.docker.com/repository/docker/kyantech/palmr/general) | **3333** (API)<br/>**5487** (Web) | • Combined backend API and frontend service<br/>• Uses local filesystem storage<br/>• Has healthcheck to ensure availability |
---
### Customizing with environment variables
You can fine-tune Palmr. using environment variables. Here's what's available for the filesystem storage setup:
| **Variable** | **Default Value** | **Description** |
| ---------------- | ------------------------------------------ | ------------------------------------------------------------ |
| `ENABLE_S3` | false | Set to 'false' for filesystem storage or 'true' for S3/MinIO |
| `ENCRYPTION_KEY` | change-this-key-in-production-min-32-chars | Required for filesystem encryption (minimum 32 characters) |
> **Important**: These variables can be set in a `.env` file at the project root or directly in your environment when running Docker Compose. The `ENCRYPTION_KEY` is crucial for securing your filesystem storage **always change it** to a unique, secure value in production. you can generate a secure key using the [KeyGenerator](/docs/3.0-beta/quick-start#generating-a-secure-encryption-key) tool.
---
### Managing persistent data
To ensure your data sticks around even if the container restarts, we use a persistent volume:
| **Volume** | **Description** |
| ------------ | ------------------------------------- |
| `palmr_data` | Stores all the data of Palmr. service |
---
## Launching Palmr.
With your `docker-compose.yaml` ready, it's time to start Palmr.! Run this command to launch everything in the background:
### Deployment
```bash
docker-compose up -d
```
This runs Palmr. in **detached mode**, meaning it operates silently in the background without flooding your terminal with logs.
Now, open your browser and visit:
```bash
http://localhost:5487
```
If you're on a server, replace `localhost` with your server's IP:
```bash
http://[YOUR_SERVER_IP]:5487
```
For example, if your server IP is `192.168.1.10`, the URL would be `http://192.168.1.10:5487`. Remember, this is just an example use your actual server IP.
> **Pro Tip**: For full functionality and security, configure your server with **HTTPS** by setting up a valid SSL certificate.
---
## Keeping Palmr. up to date
## Option 2: Bind Mounts
Want the latest features and fixes? Updating Palmr. is a breeze. Run these commands to pull the newest version from DockerHub and restart the service:
Bind mounts store data in a local directory, providing direct file system access.
### Configuration
To use bind mounts, **replace the content** of your `docker-compose.yaml` with the following configuration (you can also reference `docker-compose-bind-mount-example.yaml` as a template):
```yaml
services:
palmr:
image: kyantech/palmr:latest
container_name: palmr
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
# Optional: Set custom UID/GID for file permissions
# - PALMR_UID=1000
# - PALMR_GID=1000
ports:
- "5487:5487" # Web port
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
volumes:
# Bind mount for persistent data (uploads, database, temp files)
- ./data:/app/server # Local directory for the application data
restart: unless-stopped # Restart the container unless it is stopped
```
### Deployment
```bash
docker-compose pull
docker-compose up -d
```
That's it! You're now running the latest version of Palmr.
> **Permission Configuration**: If you encounter permission issues with bind mounts (common on NAS systems), see our [UID/GID Configuration](/docs/3.0-beta/uid-gid-configuration) guide for automatic permission handling.
---
## Running with Docker (without Compose)
## Environment Variables
Prefer to skip Docker Compose and use plain Docker? No problem! Use this command to start Palmr. directly:
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 |
> **⚠️ Security Warning**: Always change the `ENCRYPTION_KEY` in production. This key encrypts your files - losing it makes files permanently inaccessible.
> **🔗 Reverse Proxy**: If deploying behind a reverse proxy (Traefik, Nginx, etc.), set `SECURE_SITE=true` and review our [Reverse Proxy Configuration](/docs/3.0-beta/reverse-proxy-configuration) guide for proper setup.
### Generate Secure Encryption Keys
Need a strong key for `ENCRYPTION_KEY`? Use our built-in generator to create cryptographically secure keys:
<KeyGenerator />
---
## Accessing Palmr.
Once deployed, access Palmr. through your web browser:
- **Local**: `http://localhost:5487`
- **Server**: `http://YOUR_SERVER_IP:5487`
### API Access (Optional)
If you exposed port 3333 in your configuration, you can also access:
- **API Documentation**: `http://localhost:3333/docs` (local) or `http://YOUR_SERVER_IP:3333/docs` (server)
- **API Endpoints**: Available at `http://localhost:3333` (local) or `http://YOUR_SERVER_IP:3333` (server)
> **📚 Learn More**: For complete API documentation, authentication, and integration examples, see our [API Reference](/docs/3.0-beta/api) guide.
> **💡 Production Tip**: For production deployments, configure HTTPS with a valid SSL certificate for enhanced security.
---
## Docker CLI Alternative
Prefer using Docker directly? Both storage options are supported:
**Named Volume:**
```bash
docker run -d \
--name palmr \
-e ENABLE_S3=false \
-e ENCRYPTION_KEY=change-this-key-in-production-min-32-chars \
-e ENCRYPTION_KEY=your-secure-key-min-32-chars \
-p 5487:5487 \
-p 3333:3333 \
-v palmr_data:/app/server \
@@ -143,22 +175,84 @@ docker run -d \
kyantech/palmr:latest
```
This also runs in detached mode, so it's hands-off. Access Palmr. at the same URLs mentioned earlier.
**Bind Mount:**
> **Critical Reminder**: Whichever method you choose, **change the `ENCRYPTION_KEY`** to a secure, unique value. This key encrypts your files on the filesystem if you lose it, your files become inaccessible.
```bash
docker run -d \
--name palmr \
-e ENABLE_S3=false \
-e ENCRYPTION_KEY=your-secure-key-min-32-chars \
-p 5487:5487 \
-p 3333:3333 \
-v $(pwd)/data:/app/server \
--restart unless-stopped \
kyantech/palmr:latest
```
---
## Generating a Secure Encryption Key
## Maintenance
Need a strong key for `ENCRYPTION_KEY`? Use our handy Password Generator Tool below to create one:
### Updates
<KeyGenerator />
Keep Palmr. current with the latest features and security fixes:
```bash
docker-compose pull
docker-compose up -d
```
### Backup & Restore
The backup method depends on which storage option you're using:
**Named Volume Backup:**
```bash
docker run --rm \
-v palmr_data:/data \
-v $(pwd):/backup \
alpine tar czf /backup/palmr-backup.tar.gz -C /data .
```
**Named Volume Restore:**
```bash
docker run --rm \
-v palmr_data:/data \
-v $(pwd):/backup \
alpine tar xzf /backup/palmr-backup.tar.gz -C /data
```
**Bind Mount Backup:**
```bash
tar czf palmr-backup.tar.gz ./data
```
**Bind Mount Restore:**
```bash
tar xzf palmr-backup.tar.gz
```
---
## You're all set!
## Next Steps
Congratulations! You've just deployed your own secure file sharing solution with Palmr. in record time. We're thrilled to have you on board and hope you love using this powerful tool as much as we loved building it.
Your Palmr. instance is now ready! Explore additional configuration options:
Got questions or ideas? Dive into the rest of our documentation or reach out via our GitHub [issues page](https://github.com/kyantech/Palmr/issues). Happy sharing!
### Advanced Configuration
- **[UID/GID Configuration](/docs/3.0-beta/uid-gid-configuration)** - Configure user permissions for NAS systems and custom environments
- **[S3 Storage](/docs/3.0-beta/s3-configuration)** - Scale with Amazon S3 or compatible storage providers
- **[Manual Installation](/docs/3.0-beta/manual-installation)** - Manual installation and custom configurations
### Integration & Development
- **[API Reference](/docs/3.0-beta/api)** - Integrate Palmr. with your applications
- **[Architecture Guide](/docs/3.0-beta/architecture)** - Understanding Palmr. components and design
---
Need help? Visit our [GitHub Issues](https://github.com/kyantech/Palmr/issues) or community discussions.

View File

@@ -0,0 +1,199 @@
---
title: Reverse Proxy Configuration
icon: "Shield"
---
When deploying **Palmr.** behind a reverse proxy (like Traefik, Nginx, or Cloudflare), you need to configure secure cookie settings to ensure proper authentication. This guide covers the `SECURE_SITE` environment variable and related proxy configurations.
## Overview
Reverse proxies terminate SSL/TLS connections and forward requests to Palmr., which can cause authentication issues if cookies aren't configured properly for HTTPS environments. The `SECURE_SITE` environment variable controls cookie security settings to handle these scenarios.
## The SECURE_SITE Environment Variable
The `SECURE_SITE` variable configures how Palmr. handles authentication cookies based on your deployment environment:
### Configuration Options
| Value | Cookie Settings | Use Case |
| ------- | ------------------------------------- | ----------------------------------- |
| `true` | `secure: true`, `sameSite: "lax"` | HTTPS/Production with reverse proxy |
| `false` | `secure: false`, `sameSite: "strict"` | HTTP/Development (default) |
### When to Use SECURE_SITE=true
Set `SECURE_SITE=true` in the following scenarios:
- ✅ **Reverse Proxy with HTTPS**: Traefik, Nginx, HAProxy with SSL termination
- ✅ **Cloud Providers**: Cloudflare, AWS ALB, Azure Application Gateway
- ✅ **CDN with HTTPS**: Any CDN that terminates SSL
- ✅ **Production Deployments**: When users access via HTTPS
### When to Use SECURE_SITE=false
Keep `SECURE_SITE=false` (default) for:
- ✅ **Local Development**: Running on `http://localhost`
- ✅ **Direct HTTP Access**: No reverse proxy involved
- ✅ **Testing Environments**: When using HTTP
- ✅ **HTTP Reverse Proxy**: Nginx, Apache, etc. without SSL termination
---
## HTTP Reverse Proxy Setup
**Docker Compose for HTTP Nginx:**
```yaml
environment:
- SECURE_SITE=false # HTTP = false
```
> **⚠️ HTTP Security**: Remember that HTTP transmits data in plain text. Consider using HTTPS in production environments.
---
## Troubleshooting Authentication Issues
### Common Symptoms
If you experience authentication issues behind a reverse proxy:
- ❌ Login appears successful but redirects to login page
- ❌ "No Authorization was found in request.cookies" errors
- ❌ API requests return 401 Unauthorized
- ❌ User registration fails silently
### Diagnostic Steps
1. **Check Browser Developer Tools**:
- Look for cookies in Application/Storage tab
- Verify cookie has `Secure` flag when using HTTPS
- Check if `SameSite` attribute is appropriate
2. **Verify Environment Variables**:
```bash
docker exec -it palmr env | grep SECURE_SITE
```
3. **Test Cookie Settings**:
- With `SECURE_SITE=false`: Should work on HTTP
- With `SECURE_SITE=true`: Should work on HTTPS
### Common Fixes
**Problem**: Authentication fails with reverse proxy
**Solution**: Set `SECURE_SITE=true` and ensure proper headers:
```yaml
environment:
- SECURE_SITE=true
```
**Problem**: Mixed content errors
**Solution**: Ensure proxy passes correct headers:
```yaml
# Traefik
- "traefik.http.middlewares.palmr-headers.headers.customrequestheaders.X-Forwarded-Proto=https"
# Nginx
proxy_set_header X-Forwarded-Proto $scheme;
```
**Problem**: Authentication fails with HTTP reverse proxy
**Solution**: Use `SECURE_SITE=false` and ensure proper cookie headers:
```yaml
environment:
- SECURE_SITE=false # For HTTP proxy
```
```nginx
# Nginx - Add these headers for cookie handling
proxy_set_header Cookie $http_cookie;
proxy_pass_header Set-Cookie;
```
**Problem**: SQLite "readonly database" error with bind mounts
**Solution**: Configure proper UID/GID permissions:
```yaml
environment:
- PALMR_UID=1000 # Your host UID (check with: id)
- PALMR_GID=1000 # Your host GID
- ENCRYPTION_KEY=your-key-here
```
> **💡 Note**: Check your host UID/GID with `id` command and use those values. See [UID/GID Configuration](/docs/3.0-beta/uid-gid-configuration) for detailed setup.
---
## Security Considerations
> **⚠️ Important**: Always use HTTPS in production environments. The `SECURE_SITE=true` setting ensures cookies are only sent over encrypted connections.
---
## Advanced Configuration
### Multiple Domains
If serving Palmr. on multiple domains, ensure consistent cookie settings:
```yaml
environment:
- SECURE_SITE=true # Use for all HTTPS domains
```
### Development vs Production
Use environment-specific configurations:
**Development (HTTP):**
```yaml
environment:
- SECURE_SITE=false
```
**Production (HTTPS):**
```yaml
environment:
- SECURE_SITE=true
```
### Health Checks
Add health checks to ensure proper proxy configuration:
```yaml
services:
palmr:
# ... other config
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:5487/api/health"]
interval: 30s
timeout: 10s
retries: 3
```
---
## Need Help?
If you're still experiencing issues after following this guide:
1. **Check the Logs**: `docker logs palmr`
2. **Verify Headers**: Use browser dev tools or `curl -I`
3. **Test Direct Access**: Try accessing Palmr. directly (bypassing proxy)
4. **Open an Issue**: [Report bugs on GitHub](https://github.com/kyantech/Palmr/issues)
> **💡 Pro Tip**: When reporting issues, include your reverse proxy configuration and any relevant error messages from both Palmr. and your proxy logs.

View File

@@ -3,78 +3,62 @@ title: UID/GID Configuration
icon: "Users"
---
Having trouble with **permission denied** errors when using bind-mounted directories? This guide will help you configure <span className="font-bold">Palmr.</span> to work seamlessly with your host system's <span className="font-bold italic">user and group permissions</span>.
Configure user and group permissions for seamless bind mount compatibility across different host systems, particularly NAS environments.
This is particularly common on **NAS systems** like Synology or QNAP, where the container's default UID/GID doesn't match your host system. Instead of manually changing directory ownership on your host, Palmr. now supports <span className="font-bold italic">flexible UID/GID configuration</span> to automatically handle these permission conflicts.
## Overview
## The Problem
Palmr. supports runtime UID/GID configuration to resolve permission conflicts when using bind mounts. This eliminates the need for manual permission management on your host system.
By default, Palmr. runs with UID 1001 and GID 1001 inside the container. When you bind-mount a directory from your host system (like `./data:/app/server`), permission conflicts can occur if:
**Default Configuration**: UID 1001, GID 1001
- Your host user has a different UID/GID
- You're running on a NAS system with specific user configurations
- The mounted directory ownership doesn't match the container user
## When to Configure
This results in errors like:
UID/GID configuration is recommended when:
- **Access denied** even with 777 permissions
- **Database connection failures** (SQLite can't create/access files)
- **File upload failures** in the application
## The Solution
Palmr. now supports **runtime UID/GID configuration** through environment variables. The container will automatically adjust its internal user permissions to match your host system, eliminating the need to manually change directory ownership.
- Using bind mounts with different host user permissions
- Deploying on NAS systems (Synology, QNAP, etc.)
- Encountering "permission denied" errors
- Host system uses different default UID/GID values
## Environment Variables
Configure these **optional** environment variables in your `docker-compose.yml`:
Configure permissions using these optional environment variables:
| Variable | Description | Default | When to Use |
| ----------- | ---------------------------------- | ------- | ------------------------------------------- |
| `PALMR_UID` | User ID for the container process | 1001 | When host directory owner has different UID |
| `PALMR_GID` | Group ID for the container process | 1001 | When host directory group has different GID |
| Variable | Description | Default | Example |
| ----------- | -------------------------------- | ------- | ------- |
| `PALMR_UID` | User ID for container processes | `1001` | `1000` |
| `PALMR_GID` | Group ID for container processes | `1001` | `1000` |
> **Important**: These variables are **completely optional**. If you don't set them, Palmr. works exactly as before with UID/GID 1001.
---
## Finding Your Host UID/GID
## Finding Host UID/GID
Before configuring, you need to find your host system's user and group IDs:
Determine your host system's user and group IDs:
```bash
# Find your user ID
id -u
# Find your group ID
id -g
# See both together
# Check current user
id
# Output example
uid=1000(user) gid=1000(group) groups=1000(group),27(sudo)
```
Example output:
Use the `uid` and `gid` values for your configuration.
```bash
$ id
uid=1000(myuser) gid=1000(mygroup) groups=1000(mygroup),27(sudo)
```
In this example, you would use `PALMR_UID=1000` and `PALMR_GID=1000`.
---
## Configuration Examples
### Basic Linux System
Most Linux desktop systems use UID/GID 1000 for the first user:
### Standard Linux System
```yaml
services:
palmr:
image: docker.io/kyantech/palmr:latest
image: kyantech/palmr:latest
container_name: palmr
environment:
- ENABLE_S3=false
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars
# Add these lines to match your host user
- ENCRYPTION_KEY=your-secure-key-min-32-chars
- PALMR_UID=1000
- PALMR_GID=1000
ports:
@@ -86,158 +70,135 @@ services:
### Synology NAS
Synology systems commonly use these UID/GID combinations:
```yaml
services:
palmr:
image: docker.io/kyantech/palmr:latest
image: kyantech/palmr:latest
container_name: palmr
environment:
- ENABLE_S3=false
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars
# Common Synology configuration
- ENCRYPTION_KEY=your-secure-key-min-32-chars
- PALMR_UID=1026
- PALMR_GID=100
ports:
- "5487:5487"
volumes:
- /volume1/docker/palmr/data:/app/server
- /volume1/docker/palmr:/app/server
restart: unless-stopped
```
### QNAP NAS
QNAP systems typically use:
```yaml
services:
palmr:
image: docker.io/kyantech/palmr:latest
image: kyantech/palmr:latest
container_name: palmr
environment:
- ENABLE_S3=false
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars
# Common QNAP configuration
- ENCRYPTION_KEY=your-secure-key-min-32-chars
- PALMR_UID=1000
- PALMR_GID=100
ports:
- "5487:5487"
volumes:
- /share/Container/palmr/data:/app/server
- /share/Container/palmr:/app/server
restart: unless-stopped
```
### No Configuration Needed
---
If you're not experiencing permission issues, you don't need to set these variables:
## Migration Guide
```yaml
services:
palmr:
image: docker.io/kyantech/palmr:latest
container_name: palmr
environment:
- ENABLE_S3=false
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars
# No PALMR_UID/PALMR_GID needed - uses defaults
ports:
- "5487:5487"
volumes:
- ./data:/app/server
restart: unless-stopped
```
### Existing Installations
## Migration for Existing Installations
To add UID/GID configuration to running installations:
If you already have Palmr. running and want to add UID/GID configuration:
1. **Stop the container**
### Step 1: Backup Your Data
```bash
docker-compose down
```
2. **Backup your data**
```bash
cp -r ./data ./data-backup
```
3. **Update configuration**
Add UID/GID variables to your `docker-compose.yaml`
4. **Restart with new configuration**
```bash
docker-compose up -d
```
---
## Verification
### Check Configuration
Verify UID/GID settings are applied correctly:
```bash
docker-compose down
cp -r ./data ./data-backup
```
# View startup logs
docker-compose logs palmr | head -20
### Step 2: Update docker-compose.yml
Add the UID/GID environment variables to your existing configuration:
```yaml
services:
palmr:
image: docker.io/kyantech/palmr:latest
container_name: palmr
environment:
- ENABLE_S3=false
- ENCRYPTION_KEY=your-existing-key
# Add these new lines
- PALMR_UID=1000 # Your host user UID
- PALMR_GID=1000 # Your host group GID
# ... rest of your configuration
```
### Step 3: Restart Palmr.
```bash
docker-compose up -d
```
The container will automatically detect the new UID/GID configuration and adjust permissions accordingly.
## Troubleshooting
### Checking Current Configuration
To verify your container is running with the correct UID/GID:
```bash
# Check the user ID inside the container
docker exec palmr id
# Check file ownership in the mounted volume
# Check file ownership
docker exec palmr ls -la /app/server/
# Verify process UID/GID
docker exec palmr ps aux | grep node
```
### Permission Issues Persist
### Troubleshooting
If you're still having permission issues after configuration:
**Permission issues persist:**
```bash
# Check container logs for UID/GID adjustment messages
docker-compose logs palmr | grep -i "uid\|gid"
# Check environment variables
docker exec palmr env | grep PALMR
# Manually fix permissions if needed
docker exec -u root palmr chown -R palmr:nodejs /app/server
# Verify file ownership
docker exec palmr stat /app/server/prisma/palmr.db
# Review configuration logs
docker-compose logs palmr | grep -E "🔧|🔐|🔽"
```
### Finding NAS-Specific UID/GID
For NAS systems, check your system's user management interface or use SSH:
**NAS-specific debugging:**
```bash
# On your NAS, check existing users
# Synology - Check mount point ownership
ls -la /volume1/docker/palmr/
# QNAP - Check mount point ownership
ls -la /share/Container/palmr/
# Check NAS user configuration
cat /etc/passwd | grep -v nobody
# Check groups
cat /etc/group
```
## How It Works
---
When you set `PALMR_UID` and/or `PALMR_GID` environment variables:
## Implementation Details
1. **Container startup**: The container detects the environment variables
2. **User adjustment**: Automatically modifies the internal `palmr` user to use your specified UID/GID
3. **Ownership update**: Updates file ownership of application directories
4. **Conflict handling**: Gracefully handles cases where the UID/GID already exists
5. **Logging**: Provides clear feedback about what changes were made
The UID/GID configuration process:
The process is completely automatic and requires no manual intervention.
1. **Detection** - Environment variables are read during container startup
2. **Ownership Update** - File permissions are adjusted to match target UID/GID
3. **Privilege Drop** - Application runs with specified user permissions via `su-exec`
4. **Logging** - Configuration changes are logged for verification
## Build-Time Customization
This approach provides automatic permission management without user creation or system modification.
For advanced users who want to build custom images with different default UID/GID:
---
## Build-Time Configuration
For custom base images with different default values:
```bash
docker build \
@@ -246,18 +207,16 @@ docker build \
-t palmr:custom .
```
This sets new defaults at build time, which can still be overridden with environment variables at runtime.
Runtime environment variables override build-time defaults.
---
## Summary
## Benefits
The UID/GID configuration feature makes Palmr. **universally compatible** with different host systems without requiring manual permission changes. Key benefits:
- **Zero Configuration** - Works automatically when environment variables are set
- **Universal Compatibility** - Supports any valid UID/GID combination
- **NAS Optimized** - Tested with major NAS platforms
- **Backward Compatible** - Existing deployments continue without modification
- **Performance Optimized** - Lightweight implementation using `su-exec`
- ✅ **Automatic permission handling** - No manual `chown` commands needed
- ✅ **NAS system compatibility** - Tested with Synology and QNAP
- ✅ **Backward compatible** - Existing installations continue to work unchanged
- ✅ **Optional configuration** - Only use when needed
- ✅ **Runtime flexibility** - Change UID/GID without rebuilding images
If you're experiencing permission issues with bind-mounted directories, try adding the appropriate `PALMR_UID` and `PALMR_GID` environment variables to your configuration. The container will handle the rest automatically!
For permission issues with bind mounts, add the appropriate `PALMR_UID` and `PALMR_GID` environment variables to resolve conflicts automatically.

View File

@@ -1,6 +1,7 @@
# FOR FILESYSTEM STORAGE ENV VARS
ENABLE_S3=false
ENCRYPTION_KEY=change-this-key-in-production-min-32-chars
DATABASE_URL="file:./palmr.db"
# FOR USE WITH S3 COMPATIBLE STORAGE
# ENABLE_S3=true

View File

@@ -4,7 +4,7 @@ generator client {
datasource db {
provider = "sqlite"
url = "file:./palmr.db"
url = env("DATABASE_URL")
}
model User {

View File

@@ -11,6 +11,8 @@ const envSchema = z.object({
S3_REGION: z.string().optional(),
S3_BUCKET_NAME: z.string().optional(),
S3_FORCE_PATH_STYLE: z.union([z.literal("true"), z.literal("false")]).default("false"),
SECURE_SITE: z.union([z.literal("true"), z.literal("false")]).default("false"),
DATABASE_URL: z.string().optional().default("file:/app/server/prisma/palmr.db"),
});
export const env = envSchema.parse(process.env);

View File

@@ -4,7 +4,21 @@ import { FastifyReply, FastifyRequest } from "fastify";
import fs from "fs";
import path from "path";
const uploadsDir = path.join(process.cwd(), "uploads/logo");
const isDocker = (() => {
try {
require("fs").statSync("/.dockerenv");
return true;
} catch {
try {
return require("fs").readFileSync("/proc/self/cgroup", "utf8").includes("docker");
} catch {
return false;
}
}
})();
const baseDir = isDocker ? "/app/server" : process.cwd();
const uploadsDir = path.join(baseDir, "uploads/logo");
if (!fs.existsSync(uploadsDir)) {
fs.mkdirSync(uploadsDir, { recursive: true });
}

View File

@@ -1,3 +1,4 @@
import { env } from "../../env";
import { LoginSchema, RequestPasswordResetSchema, createResetPasswordSchema } from "./dto";
import { AuthService } from "./service";
import { FastifyReply, FastifyRequest } from "fastify";
@@ -17,8 +18,8 @@ export class AuthController {
reply.setCookie("token", token, {
httpOnly: true,
path: "/",
secure: false,
sameSite: "strict",
secure: env.SECURE_SITE === "true" ? true : false,
sameSite: env.SECURE_SITE === "true" ? "lax" : "strict",
});
return reply.send({ user });

View File

@@ -2,12 +2,35 @@ import { ConfigService } from "../config/service";
import { PrismaClient } from "@prisma/client";
import { exec } from "child_process";
import { promisify } from "util";
import fs from 'node:fs';
const execAsync = promisify(exec);
const prisma = new PrismaClient();
export class StorageService {
private configService = new ConfigService();
private isDockerCached = undefined;
private _hasDockerEnv() {
try {
fs.statSync('/.dockerenv');
return true;
} catch {
return false;
}
}
private _hasDockerCGroup() {
try {
return fs.readFileSync('/proc/self/cgroup', 'utf8').includes('docker');
} catch {
return false;
}
}
private _isDocker() {
return this.isDockerCached ?? (this._hasDockerEnv() || this._hasDockerCGroup());
}
async getDiskSpace(
userId?: string,
@@ -20,11 +43,14 @@ export class StorageService {
}> {
try {
if (isAdmin) {
const command = process.platform === "win32"
? "wmic logicaldisk get size,freespace,caption"
: process.platform === "darwin"
? "df -k ."
: "df -B1 .";
const isDocker = this._isDocker();
const pathToCheck = isDocker ? "/app/server/uploads" : ".";
const command = process.platform === "win32"
? "wmic logicaldisk get size,freespace,caption"
: process.platform === "darwin"
? `df -k ${pathToCheck}`
: `df -B1 ${pathToCheck}`;
const { stdout } = await execAsync(command);
let total = 0;

View File

@@ -14,20 +14,26 @@ export async function userRoutes(app: FastifyInstance) {
const usersCount = await prisma.user.count();
if (usersCount > 0) {
await request.jwtVerify();
if (!request.user.isAdmin) {
try {
await request.jwtVerify();
if (!request.user.isAdmin) {
return reply
.status(403)
.send({ error: "Access restricted to administrators" })
.description("Access restricted to administrators");
}
} catch (authErr) {
console.error(authErr);
return reply
.status(403)
.send({ error: "Access restricted to administrators" })
.description("Access restricted to administrators");
.status(401)
.send({ error: "Unauthorized: a valid token is required to access this resource." })
.description("Unauthorized: a valid token is required to access this resource.");
}
}
// If usersCount is 0, allow the request to proceed without authentication
} catch (err) {
console.error(err);
return reply
.status(401)
.send({ error: "Unauthorized: a valid token is required to access this resource." })
.description("Unauthorized: a valid token is required to access this resource.");
return reply.status(500).send({ error: "Internal server error" }).description("Internal server error");
}
};

View File

@@ -1,5 +1,6 @@
import { env } from "../env";
import { StorageProvider } from "../types/storage";
import { IS_RUNNING_IN_CONTAINER } from "../utils/container-detection";
import * as crypto from "crypto";
import * as fsSync from "fs";
import * as fs from "fs/promises";
@@ -9,12 +10,14 @@ import { pipeline } from "stream/promises";
export class FilesystemStorageProvider implements StorageProvider {
private static instance: FilesystemStorageProvider;
private uploadsDir = path.join(process.cwd(), "uploads");
private uploadsDir: string;
private encryptionKey = env.ENCRYPTION_KEY;
private uploadTokens = new Map<string, { objectName: string; expiresAt: number }>();
private downloadTokens = new Map<string, { objectName: string; expiresAt: number; fileName?: string }>();
private constructor() {
this.uploadsDir = IS_RUNNING_IN_CONTAINER ? "/app/server/uploads" : path.join(process.cwd(), "uploads");
this.ensureUploadsDir();
setInterval(() => this.cleanExpiredTokens(), 5 * 60 * 1000);
}
@@ -214,8 +217,10 @@ export class FilesystemStorageProvider implements StorageProvider {
if (encryptedBuffer.length > 16) {
try {
return this.decryptFileBuffer(encryptedBuffer);
} catch (error) {
console.warn("Failed to decrypt with new method, trying legacy format");
} catch (error: unknown) {
if (error instanceof Error) {
console.warn("Failed to decrypt with new method, trying legacy format", error.message);
}
return this.decryptFileLegacy(encryptedBuffer);
}
}

View File

@@ -11,6 +11,7 @@ import { reverseShareRoutes } from "./modules/reverse-share/routes";
import { shareRoutes } from "./modules/share/routes";
import { storageRoutes } from "./modules/storage/routes";
import { userRoutes } from "./modules/user/routes";
import { IS_RUNNING_IN_CONTAINER } from "./utils/container-detection";
import fastifyMultipart from "@fastify/multipart";
import fastifyStatic from "@fastify/static";
import * as fs from "fs/promises";
@@ -26,21 +27,22 @@ if (typeof global.crypto === "undefined") {
}
async function ensureDirectories() {
const uploadsDir = path.join(process.cwd(), "uploads");
const tempChunksDir = path.join(process.cwd(), "temp-chunks");
const baseDir = IS_RUNNING_IN_CONTAINER ? "/app/server" : process.cwd();
const uploadsDir = path.join(baseDir, "uploads");
const tempChunksDir = path.join(baseDir, "temp-chunks");
try {
await fs.access(uploadsDir);
} catch {
await fs.mkdir(uploadsDir, { recursive: true });
console.log("📁 Created uploads directory");
console.log(`📁 Created uploads directory: ${uploadsDir}`);
}
try {
await fs.access(tempChunksDir);
} catch {
await fs.mkdir(tempChunksDir, { recursive: true });
console.log("📁 Created temp-chunks directory");
console.log(`📁 Created temp-chunks directory: ${tempChunksDir}`);
}
}
@@ -62,8 +64,11 @@ async function startServer() {
});
if (env.ENABLE_S3 !== "true") {
const baseDir = IS_RUNNING_IN_CONTAINER ? "/app/server" : process.cwd();
const uploadsPath = path.join(baseDir, "uploads");
await app.register(fastifyStatic, {
root: path.join(process.cwd(), "uploads"),
root: uploadsPath,
prefix: "/uploads/",
decorateReply: false,
});

View File

@@ -0,0 +1,45 @@
import * as fsSync from "fs";
/**
* Determines if the application is running inside a container environment.
* Checks common container indicators like /.dockerenv and cgroup file patterns.
*
* This function caches its result after the first call for performance.
*
* @returns {boolean} True if running in a container, false otherwise.
*/
function isRunningInContainer(): boolean {
try {
if (fsSync.existsSync("/.dockerenv")) {
return true;
}
const cgroupContent = fsSync.readFileSync("/proc/self/cgroup", "utf8");
const containerPatterns = [
"docker",
"containerd",
"lxc",
"kubepods",
"pod",
"/containers/",
"system.slice/container-",
];
for (const pattern of containerPatterns) {
if (cgroupContent.includes(pattern)) {
return true;
}
}
if (fsSync.existsSync("/.well-known/container")) {
return true;
}
} catch (e: unknown) {
if (e instanceof Error) {
console.warn("Could not perform full container detection:", e.message);
}
}
return false;
}
export const IS_RUNNING_IN_CONTAINER = isRunningInContainer();

View File

@@ -2,12 +2,12 @@ import { useState } from "react";
import { IconDownload, IconEye } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { FilePreviewModal } from "@/components/modals/file-preview-modal";
import { Button } from "@/components/ui/button";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { getFileIcon } from "@/utils/file-icons";
import { formatFileSize } from "@/utils/format-file-size";
import { ShareFilesTableProps } from "../types";
import { ShareFilePreviewModal } from "./share-file-preview-modal";
export function ShareFilesTable({ files, onDownload }: ShareFilesTableProps) {
const t = useTranslations();
@@ -99,7 +99,14 @@ export function ShareFilesTable({ files, onDownload }: ShareFilesTableProps) {
</Table>
</div>
{selectedFile && <FilePreviewModal isOpen={isPreviewOpen} onClose={handleClosePreview} file={selectedFile} />}
{selectedFile && (
<ShareFilePreviewModal
isOpen={isPreviewOpen}
onClose={handleClosePreview}
file={selectedFile}
onDownload={onDownload}
/>
)}
</div>
);
}

View File

@@ -0,0 +1,320 @@
"use client";
import { useEffect, useState } from "react";
import { IconDownload } from "@tabler/icons-react";
import { useTranslations } from "next-intl";
import { toast } from "sonner";
import { CustomAudioPlayer } from "@/components/audio/custom-audio-player";
import { AspectRatio } from "@/components/ui/aspect-ratio";
import { Button } from "@/components/ui/button";
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog";
import { ScrollArea } from "@/components/ui/scroll-area";
import { getDownloadUrl } from "@/http/endpoints";
import { getFileIcon } from "@/utils/file-icons";
interface ShareFilePreviewModalProps {
isOpen: boolean;
onClose: () => void;
file: {
name: string;
objectName: string;
type?: string;
};
onDownload: (objectName: string, fileName: string) => void;
}
export function ShareFilePreviewModal({ isOpen, onClose, file, onDownload }: ShareFilePreviewModalProps) {
const t = useTranslations();
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [videoBlob, setVideoBlob] = useState<string | null>(null);
const [pdfAsBlob, setPdfAsBlob] = useState(false);
const [downloadUrl, setDownloadUrl] = useState<string | null>(null);
const [pdfLoadFailed, setPdfLoadFailed] = useState(false);
const [isLoadingPreview, setIsLoadingPreview] = useState(false);
useEffect(() => {
if (isOpen && file.objectName && !isLoadingPreview) {
setIsLoading(true);
setPreviewUrl(null);
setVideoBlob(null);
setPdfAsBlob(false);
setDownloadUrl(null);
setPdfLoadFailed(false);
loadPreview();
}
}, [file.objectName, isOpen]);
useEffect(() => {
return () => {
if (previewUrl && previewUrl.startsWith("blob:")) {
URL.revokeObjectURL(previewUrl);
}
if (videoBlob && videoBlob.startsWith("blob:")) {
URL.revokeObjectURL(videoBlob);
}
};
}, [previewUrl, videoBlob]);
useEffect(() => {
if (!isOpen) {
if (previewUrl && previewUrl.startsWith("blob:")) {
URL.revokeObjectURL(previewUrl);
setPreviewUrl(null);
}
if (videoBlob && videoBlob.startsWith("blob:")) {
URL.revokeObjectURL(videoBlob);
setVideoBlob(null);
}
}
}, [isOpen]);
const loadPreview = async () => {
if (!file.objectName || isLoadingPreview) return;
setIsLoadingPreview(true);
try {
const encodedObjectName = encodeURIComponent(file.objectName);
const response = await getDownloadUrl(encodedObjectName);
const url = response.data.url;
setDownloadUrl(url);
const fileType = getFileType();
if (fileType === "video") {
await loadVideoPreview(url);
} else if (fileType === "audio") {
await loadAudioPreview(url);
} else if (fileType === "pdf") {
await loadPdfPreview(url);
} else {
setPreviewUrl(url);
}
} catch (error) {
console.error("Failed to load preview:", error);
toast.error(t("filePreview.loadError"));
} finally {
setIsLoading(false);
setIsLoadingPreview(false);
}
};
const loadVideoPreview = async (url: string) => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
setVideoBlob(blobUrl);
} catch (error) {
console.error("Failed to load video as blob:", error);
setPreviewUrl(url);
}
};
const loadAudioPreview = async (url: string) => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
const blobUrl = URL.createObjectURL(blob);
setPreviewUrl(blobUrl);
} catch (error) {
console.error("Failed to load audio as blob:", error);
setPreviewUrl(url);
}
};
const loadPdfPreview = async (url: string) => {
try {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const blob = await response.blob();
const finalBlob = new Blob([blob], { type: "application/pdf" });
const blobUrl = URL.createObjectURL(finalBlob);
setPreviewUrl(blobUrl);
setPdfAsBlob(true);
} catch (error) {
console.error("Failed to load PDF as blob:", error);
setPreviewUrl(url);
setTimeout(() => {
if (!pdfLoadFailed && !pdfAsBlob) {
handlePdfLoadError();
}
}, 4000);
}
};
const handlePdfLoadError = async () => {
if (pdfLoadFailed || pdfAsBlob) return;
setPdfLoadFailed(true);
if (downloadUrl) {
setTimeout(() => {
loadPdfPreview(downloadUrl);
}, 500);
}
};
const handleDownload = () => {
onDownload(file.objectName, file.name);
};
const getFileType = () => {
const extension = file.name.split(".").pop()?.toLowerCase();
if (extension === "pdf") return "pdf";
if (["jpg", "jpeg", "png", "gif", "webp", "svg", "bmp", "tiff"].includes(extension || "")) return "image";
if (["mp3", "wav", "ogg", "m4a", "aac", "flac"].includes(extension || "")) return "audio";
if (["mp4", "webm", "ogg", "mov", "avi", "mkv", "wmv", "flv", "m4v"].includes(extension || "")) return "video";
return "other";
};
const renderPreview = () => {
const fileType = getFileType();
const { icon: FileIcon, color } = getFileIcon(file.name);
if (isLoading) {
return (
<div className="flex flex-col items-center justify-center h-96 gap-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
<p className="text-muted-foreground">{t("filePreview.loading")}</p>
</div>
);
}
const mediaUrl = fileType === "video" ? videoBlob : previewUrl;
if (!mediaUrl && (fileType === "video" || fileType === "audio")) {
return (
<div className="flex flex-col items-center justify-center h-96 gap-4">
<FileIcon className={`h-12 w-12 ${color}`} />
<p className="text-muted-foreground">{t("filePreview.notAvailable")}</p>
<p className="text-sm text-muted-foreground">{t("filePreview.downloadToView")}</p>
</div>
);
}
if (!previewUrl && fileType !== "video") {
return (
<div className="flex flex-col items-center justify-center h-96 gap-4">
<FileIcon className={`h-12 w-12 ${color}`} />
<p className="text-muted-foreground">{t("filePreview.notAvailable")}</p>
<p className="text-sm text-muted-foreground">{t("filePreview.downloadToView")}</p>
</div>
);
}
switch (fileType) {
case "pdf":
return (
<ScrollArea className="w-full">
<div className="w-full min-h-[600px] border rounded-lg overflow-hidden bg-card">
{pdfAsBlob ? (
<iframe
src={`${previewUrl!}#toolbar=0&navpanes=0&scrollbar=0&view=FitH`}
className="w-full h-full min-h-[600px]"
title={file.name}
style={{ border: "none" }}
/>
) : pdfLoadFailed ? (
<div className="flex items-center justify-center h-full min-h-[600px]">
<div className="flex flex-col items-center gap-4">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
<p className="text-muted-foreground">{t("filePreview.loadingAlternative")}</p>
</div>
</div>
) : (
<div className="w-full h-full min-h-[600px] relative">
<object
data={`${previewUrl!}#toolbar=0&navpanes=0&scrollbar=0&view=FitH`}
type="application/pdf"
className="w-full h-full min-h-[600px]"
onError={handlePdfLoadError}
>
<iframe
src={`${previewUrl!}#toolbar=0&navpanes=0&scrollbar=0&view=FitH`}
className="w-full h-full min-h-[600px]"
title={file.name}
style={{ border: "none" }}
onError={handlePdfLoadError}
/>
</object>
</div>
)}
</div>
</ScrollArea>
);
case "image":
return (
<AspectRatio ratio={16 / 9} className="bg-muted">
<img src={previewUrl!} alt={file.name} className="object-contain w-full h-full rounded-md" />
</AspectRatio>
);
case "audio":
return (
<div className="flex flex-col items-center justify-center gap-6 py-4">
<CustomAudioPlayer src={mediaUrl!} />
</div>
);
case "video":
return (
<div className="flex flex-col items-center justify-center gap-4 py-6">
<div className="w-full max-w-4xl">
<video controls className="w-full rounded-lg" preload="metadata" style={{ maxHeight: "70vh" }}>
<source src={mediaUrl!} />
{t("filePreview.videoNotSupported")}
</video>
</div>
</div>
);
default:
return (
<div className="flex flex-col items-center justify-center h-96 gap-4">
<FileIcon className={`text-6xl ${color}`} />
<p className="text-muted-foreground">{t("filePreview.notAvailable")}</p>
<p className="text-sm text-muted-foreground">{t("filePreview.downloadToView")}</p>
</div>
);
}
};
return (
<Dialog open={isOpen} onOpenChange={onClose}>
<DialogContent className="sm:max-w-3xl max-h-[90vh] overflow-hidden flex flex-col">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
{(() => {
const FileIcon = getFileIcon(file.name).icon;
return <FileIcon size={24} />;
})()}
<span className="truncate">{file.name}</span>
</DialogTitle>
</DialogHeader>
<div className="flex-1 overflow-auto">{renderPreview()}</div>
<DialogFooter>
<Button variant="outline" onClick={onClose}>
{t("common.close")}
</Button>
<Button onClick={handleDownload}>
<IconDownload className="h-4 w-4" />
{t("common.download")}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@@ -51,11 +51,11 @@ export function usePublicShare() {
await loadShare(password);
};
const handleDownload = async (file: { id: string; name: string }) => {
const handleDownload = async (objectName: string, fileName: string) => {
try {
const response = await getDownloadUrl(file.id);
const encodedObjectName = encodeURIComponent(objectName);
const response = await getDownloadUrl(encodedObjectName);
const downloadUrl = response.data.url;
const fileName = downloadUrl.split("/").pop() || file.name;
const link = document.createElement("a");
link.href = downloadUrl;
@@ -63,6 +63,7 @@ export function usePublicShare() {
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success(t("share.messages.downloadStarted"));
} catch (error) {
toast.error(t("share.errors.downloadFailed"));
}

View File

@@ -3,12 +3,15 @@ import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest, { params }: { params: Promise<{ shareId: string }> }) {
const cookieHeader = req.headers.get("cookie");
const { shareId } = await params;
const body = await req.text();
const apiRes = await fetch(`${process.env.API_BASE_URL}/shares/${shareId}/recipients/notify`, {
const apiRes = await fetch(`${process.env.API_BASE_URL}/shares/${shareId}/notify`, {
method: "POST",
headers: {
"Content-Type": "application/json",
cookie: cookieHeader || "",
},
body: body,
redirect: "manual",
});

View File

@@ -14,8 +14,8 @@ export default function AuthCallbackPage() {
if (token) {
Cookies.set("token", token, {
path: "/",
secure: false,
sameSite: "strict",
secure: window.location.protocol === "https:",
sameSite: window.location.protocol === "https:" ? "lax" : "strict",
httpOnly: false,
});

View File

@@ -49,6 +49,17 @@ export function useLogin() {
useEffect(() => {
const checkAuth = async () => {
try {
const appInfoResponse = await fetch("/api/app/info");
const appInfo = await appInfoResponse.json();
if (appInfo.firstUserAccess) {
setUser(null);
setIsAdmin(false);
setIsAuthenticated(false);
setIsInitialized(true);
return;
}
const userResponse = await getCurrentUser();
if (!userResponse?.data?.user) {
throw new Error("No user data");

View File

@@ -49,7 +49,7 @@ export function LanguageSwitcher() {
maxAge: COOKIE_MAX_AGE,
path: "/",
sameSite: "lax",
secure: false,
secure: window.location.protocol === "https:",
});
router.refresh();

View File

@@ -41,6 +41,16 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
useEffect(() => {
const checkAuth = async () => {
try {
const appInfoResponse = await fetch("/api/app/info");
const appInfo = await appInfoResponse.json();
if (appInfo.firstUserAccess) {
setUser(null);
setIsAdmin(false);
setIsAuthenticated(false);
return;
}
const response = await getCurrentUser();
if (!response?.data?.user) {
throw new Error("No user data");

View File

@@ -0,0 +1,17 @@
services:
palmr:
image: kyantech/palmr:latest
container_name: palmr
environment:
- ENABLE_S3=false
- ENCRYPTION_KEY=change-this-key-in-production-min-32-chars # CHANGE THIS KEY FOR SECURITY
# Optional: Set custom UID/GID for file permissions
# - PALMR_UID=1000
# - PALMR_GID=1000
ports:
- "5487:5487" # Web port
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
volumes:
# Bind mount for persistent data (uploads, database, temp files)
- ./data:/app/server # Volume for the application data
restart: unless-stopped # Restart the container unless it is stopped

View File

@@ -9,7 +9,7 @@ services:
- "5487:5487" # Web port
- "3333:3333" # API port (OPTIONAL EXPOSED - ONLY IF YOU WANT TO ACCESS THE API DIRECTLY)
volumes:
- palmr_data:/app/server # Volume for the application data
- 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
volumes:

View File

@@ -1,22 +1,133 @@
#!/bin/sh
set -e
echo "Starting Palmr Server with SQLite..."
echo "🌴 Starting Palmr Server..."
# Set proper environment
export HOME=/home/palmr
export NPM_CONFIG_CACHE=/home/palmr/.npm
export PNPM_HOME=/home/palmr/.pnpm
# === UID/GID Runtime Configuration ===
TARGET_UID=${PALMR_UID:-1001}
TARGET_GID=${PALMR_GID:-1001}
cd /app/server
if [ -n "$PALMR_UID" ] || [ -n "$PALMR_GID" ]; then
echo "🔧 Runtime UID/GID: $TARGET_UID:$TARGET_GID"
# Update ownership of critical directories to match target UID/GID
echo "🔐 Updating file ownership..."
chown -R $TARGET_UID:$TARGET_GID /app/palmr-app 2>/dev/null || echo "⚠️ Some ownership changes may have failed"
chown -R $TARGET_UID:$TARGET_GID /home/palmr 2>/dev/null || echo "⚠️ Some home directory ownership changes may have failed"
# Update ownership of data directory if it exists
if [ -d "/app/server" ]; then
chown -R $TARGET_UID:$TARGET_GID /app/server 2>/dev/null || echo "⚠️ Some data directory ownership changes may have failed"
fi
echo "✅ UID/GID configuration completed"
fi
echo "Generating Prisma client..."
npx prisma generate --schema=./prisma/schema.prisma
# Ensure we're in the correct directory
cd /app/palmr-app
echo "Pushing database schema..."
npx prisma db push --accept-data-loss
# Set the database URL
export DATABASE_URL="file:/app/server/prisma/palmr.db"
echo "Running database seeds..."
node prisma/seed.js || echo "Seeds failed or already exist, continuing..."
echo "📂 Data directory: /app/server"
echo "💾 Database: $DATABASE_URL"
echo "Starting server application..."
exec node dist/server.js
# Create all necessary directories
echo "📁 Creating data directories..."
mkdir -p /app/server/prisma /app/server/uploads /app/server/temp-chunks /app/server/uploads/logo
# Fix ownership of database directory BEFORE database operations
if [ "$(id -u)" = "0" ]; then
echo "🔐 Ensuring proper ownership before database operations..."
chown -R $TARGET_UID:$TARGET_GID /app/server/prisma 2>/dev/null || true
fi
# Check if it's a first run (no database file exists)
if [ ! -f "/app/server/prisma/palmr.db" ]; then
echo "🚀 First run detected - setting up database..."
# Create database with proper schema path - run as target user to avoid permission issues
echo "🗄️ Creating database schema..."
if [ "$(id -u)" = "0" ]; then
su-exec $TARGET_UID:$TARGET_GID npx prisma db push --schema=./prisma/schema.prisma --skip-generate
else
npx prisma db push --schema=./prisma/schema.prisma --skip-generate
fi
# Run seed script from application directory (where node_modules is) - as target user
echo "🌱 Seeding database..."
if [ "$(id -u)" = "0" ]; then
su-exec $TARGET_UID:$TARGET_GID node ./prisma/seed.js
else
node ./prisma/seed.js
fi
echo "✅ Database setup completed!"
else
echo "♻️ Existing database found"
# Always run migrations to ensure schema is up to date - as target user
echo "🔧 Checking for schema updates..."
if [ "$(id -u)" = "0" ]; then
su-exec $TARGET_UID:$TARGET_GID npx prisma db push --schema=./prisma/schema.prisma --skip-generate
else
npx prisma db push --schema=./prisma/schema.prisma --skip-generate
fi
# Check if configurations exist - as target user
echo "🔍 Verifying database configurations..."
CONFIG_COUNT=$(
if [ "$(id -u)" = "0" ]; then
su-exec $TARGET_UID:$TARGET_GID node -e "
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
prisma.appConfig.count()
.then(count => {
console.log(count);
process.exit(0);
})
.catch(() => {
console.log(0);
process.exit(0);
});
" 2>/dev/null || echo "0"
else
node -e "
const { PrismaClient } = require('@prisma/client');
const prisma = new PrismaClient();
prisma.appConfig.count()
.then(count => {
console.log(count);
process.exit(0);
})
.catch(() => {
console.log(0);
process.exit(0);
});
" 2>/dev/null || echo "0"
fi
)
if [ "$CONFIG_COUNT" -eq "0" ]; then
echo "🌱 No configurations found, running seed..."
# Always run seed from application directory where node_modules is available - as target user
if [ "$(id -u)" = "0" ]; then
su-exec $TARGET_UID:$TARGET_GID node ./prisma/seed.js
else
node ./prisma/seed.js
fi
else
echo "✅ Found $CONFIG_COUNT configurations"
fi
fi
echo "🚀 Starting Palmr server..."
# Drop privileges using su-exec with specific UID/GID
if [ "$(id -u)" = "0" ]; then
echo "🔽 Dropping privileges to UID:GID $TARGET_UID:$TARGET_GID"
exec su-exec $TARGET_UID:$TARGET_GID node dist/server.js
else
# We're already running as non-root
exec node dist/server.js
fi

34
infra/supervisord.conf Normal file
View File

@@ -0,0 +1,34 @@
[supervisord]
nodaemon=true
user=root
logfile=/dev/stdout
logfile_maxbytes=0
pidfile=/var/run/supervisord.pid
loglevel=info
[program:server]
command=/bin/sh -c "export DATABASE_URL='file:/app/server/prisma/palmr.db' && export UPLOAD_PATH='/app/server/uploads' && export TEMP_CHUNKS_PATH='/app/server/temp-chunks' && /app/server-start.sh"
directory=/app/palmr-app
user=root
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
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'
directory=/app/web
user=palmr
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
environment=PORT=5487,HOSTNAME="0.0.0.0",HOME="/home/palmr",API_BASE_URL="http://127.0.0.1:3333"
priority=200
startsecs=10