From 6ca1ac42944f575af6d5a49197c87867d503fd1a Mon Sep 17 00:00:00 2001 From: Dionisio Date: Thu, 12 Feb 2026 09:57:33 +0100 Subject: [PATCH] fix: resolve Docker volume permission denied on startup Root cause: Docker named volumes are created as root, but the container ran as the unprivileged 'oxicloud' user (UID 1001). Services like thumbnail_service, image_transcode, and dedup_service call create_dir_all under /app/storage during initialization, which fails with 'Permission denied (os error 13)'. Changes: - Add entrypoint.sh that runs as root to chown /app/storage, then drops privileges via su-exec before executing the application - Update Dockerfile to install su-exec, copy entrypoint, and use ENTRYPOINT instead of USER+CMD - Downgrade id_mapping_service initial write failure from ERROR to WARN (empty in-memory map is perfectly valid, will persist on next save) - Improve panic message in main.rs to hint at Docker permission issue Fixes # --- Dockerfile | 17 +++++++++----- entrypoint.sh | 23 +++++++++++++++++++ .../services/id_mapping_service.rs | 4 ++-- src/main.rs | 2 +- 4 files changed, 37 insertions(+), 9 deletions(-) create mode 100755 entrypoint.sh diff --git a/Dockerfile b/Dockerfile index ba6e6583..c9820a28 100644 --- a/Dockerfile +++ b/Dockerfile @@ -37,8 +37,9 @@ LABEL org.opencontainers.image.title="OxiCloud" \ org.opencontainers.image.licenses="MIT" # Install only necessary runtime dependencies and update packages +# su-exec is needed by the entrypoint to drop privileges after fixing volume permissions RUN apk --no-cache upgrade && \ - apk add --no-cache libgcc ca-certificates libpq tzdata + apk add --no-cache libgcc ca-certificates libpq tzdata su-exec # Create non-root user RUN addgroup -g 1001 -S oxicloud && \ @@ -48,12 +49,16 @@ RUN addgroup -g 1001 -S oxicloud && \ COPY --from=builder /app/target/release/oxicloud /usr/local/bin/ RUN chmod +x /usr/local/bin/oxicloud +# Copy entrypoint script +COPY entrypoint.sh /usr/local/bin/entrypoint.sh +RUN chmod +x /usr/local/bin/entrypoint.sh + # Copy static files and other resources needed at runtime COPY --chown=oxicloud:oxicloud static /app/static COPY --chown=oxicloud:oxicloud db /app/db # Create storage directory with proper permissions -RUN mkdir -p /app/storage && chown oxicloud:oxicloud /app/storage +RUN mkdir -p /app/storage && chown -R oxicloud:oxicloud /app/storage # Set working directory WORKDIR /app @@ -61,8 +66,8 @@ WORKDIR /app # Expose application port EXPOSE 8086 -# Run as non-root user -USER oxicloud - -# Run the application +# Entrypoint fixes volume permissions then drops to oxicloud user. +# The container starts as root so it can chown mounted volumes, +# then su-exec drops privileges before running the application. +ENTRYPOINT ["entrypoint.sh"] CMD ["oxicloud"] diff --git a/entrypoint.sh b/entrypoint.sh new file mode 100755 index 00000000..c9d83f19 --- /dev/null +++ b/entrypoint.sh @@ -0,0 +1,23 @@ +#!/bin/sh +set -e + +# Fix ownership of mounted volumes. +# When Docker creates named volumes they are owned by root, but the +# application runs as the unprivileged "oxicloud" user (UID 1001). +# This script runs as root, fixes permissions, then drops privileges. + +STORAGE_DIR="/app/storage" +STATIC_DIR="/app/static" + +# Ensure the storage directory exists and is writable by oxicloud +if [ -d "$STORAGE_DIR" ]; then + chown -R oxicloud:oxicloud "$STORAGE_DIR" +fi + +# Ensure static directory is readable +if [ -d "$STATIC_DIR" ]; then + chown -R oxicloud:oxicloud "$STATIC_DIR" +fi + +# Drop privileges and exec the main binary (or whatever was passed as CMD) +exec su-exec oxicloud "$@" diff --git a/src/infrastructure/services/id_mapping_service.rs b/src/infrastructure/services/id_mapping_service.rs index eb0ea812..0f921a8f 100644 --- a/src/infrastructure/services/id_mapping_service.rs +++ b/src/infrastructure/services/id_mapping_service.rs @@ -181,11 +181,11 @@ impl IdMappingService { } } - // Write empty map to file + // Write empty map to file (best-effort: the in-memory map is valid even if disk write fails) match serde_json::to_string_pretty(&empty_map) { Ok(json) => { if let Err(e) = fs::write(map_path, json).await { - tracing::error!("Failed to write initial empty ID map: {}", e); + tracing::warn!("Could not write initial empty ID map (will retry on next save): {}", e); } else { tracing::info!("Created initial empty ID map at {}", map_path.display()); } diff --git a/src/main.rs b/src/main.rs index 8f9ba9a2..7ec8aa4e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -85,7 +85,7 @@ async fn main() -> Result<(), Box> { ); let app_state = factory.build_app_state(db_pool).await - .expect("Failed to build application state"); + .expect("Failed to build application state. If running in Docker, ensure the storage volume is writable by the oxicloud user (UID 1001)"); // Build application router let api_routes = create_api_routes(&app_state);