From 4db6a83a458751aadd3c2753b02797ea28cf2d00 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sun, 11 Jan 2026 20:14:29 -0800 Subject: [PATCH] refactor(Docker): remove legacy Docker setup and streamline server deployment This commit removes the outdated Dockerfile, docker-compose.yml, and related documentation for the Spacedrive server, consolidating the deployment process into a single Dockerfile located in the apps/server directory. The new setup supports multi-architecture builds and includes enhanced media processing capabilities. Additionally, a self-hosting guide is introduced to assist users in deploying the server on their infrastructure, ensuring a more efficient and user-friendly experience. --- .github/workflows/release.yml | 71 +++- .github/workflows/server.yml | 20 +- DOCKER.md | 158 --------- Dockerfile | 76 ----- README.md | 3 +- SERVER_RELEASE_SETUP.md | 302 +++++++++++++++++ apps/server/Dockerfile | 32 +- core/src/infra/action/manager.rs | 4 +- core/src/infra/action/mod.rs | 10 +- core/src/ops/files/copy/action.rs | 18 +- core/src/ops/files/copy/job.rs | 7 + core/src/ops/files/create_folder/action.rs | 2 +- core/src/ops/files/delete/action.rs | 2 +- core/src/ops/files/rename/action.rs | 2 +- core/src/ops/indexing/action.rs | 2 +- core/src/ops/libraries/create/action.rs | 2 +- core/src/ops/libraries/open/action.rs | 2 +- core/src/ops/libraries/rename/action.rs | 2 +- core/src/ops/locations/add/action.rs | 2 +- .../ops/locations/enable_indexing/action.rs | 2 +- core/src/ops/locations/import/action.rs | 2 +- core/src/ops/locations/trigger_job/action.rs | 2 +- core/src/ops/locations/update/action.rs | 2 +- core/src/ops/network/sync_setup/action.rs | 4 +- core/src/ops/spaces/add_group/action.rs | 2 +- core/src/ops/spaces/add_item/action.rs | 2 +- core/src/ops/spaces/create/action.rs | 2 +- core/src/ops/spaces/delete/action.rs | 2 +- core/src/ops/spaces/delete_group/action.rs | 2 +- core/src/ops/spaces/delete_item/action.rs | 2 +- core/src/ops/spaces/reorder/action.rs | 4 +- core/src/ops/spaces/update/action.rs | 2 +- core/src/ops/spaces/update_group/action.rs | 2 +- docker-compose.yml | 38 --- docs/mint.json | 1 + docs/overview/self-hosting.mdx | 317 ++++++++++++++++++ .../JobManager/JobManagerPopover.tsx | 3 + .../JobManager/components/JobCard.tsx | 4 +- .../components/JobManager/hooks/useJobs.ts | 8 +- .../src/routes/overview/DevicePanel.tsx | 3 +- 40 files changed, 791 insertions(+), 332 deletions(-) delete mode 100644 DOCKER.md delete mode 100644 Dockerfile create mode 100644 SERVER_RELEASE_SETUP.md delete mode 100644 docker-compose.yml create mode 100644 docs/overview/self-hosting.mdx diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d7656a9b7..bb5b86d96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -102,6 +102,71 @@ jobs: # name: cli-${{ matrix.platform }} # path: dist/* + # Server builds for self-hosting + server-build: + strategy: + matrix: + settings: + - host: ubuntu-22.04 + target: x86_64-unknown-linux-gnu + platform: linux-x86_64 + - host: ubuntu-22.04 + target: aarch64-unknown-linux-gnu + platform: linux-aarch64 + name: Server - ${{ matrix.settings.platform }} + runs-on: ${{ matrix.settings.host }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.settings.target }} + + - name: Setup System and Rust + uses: ./.github/actions/setup-system + with: + token: ${{ secrets.GITHUB_TOKEN }} + target: ${{ matrix.settings.target }} + + - name: Install cross-compilation tools (ARM) + if: matrix.settings.target == 'aarch64-unknown-linux-gnu' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - name: Setup native dependencies + run: cargo run -p xtask -- setup + + - name: Build server binary + run: | + cargo build --release --bin sd-server --features sd-core/heif,sd-core/ffmpeg --target ${{ matrix.settings.target }} + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + + - name: Prepare server binary + run: | + mkdir -p dist + cp target/${{ matrix.settings.target }}/release/sd-server dist/sd-server-${{ matrix.settings.platform }} + chmod +x dist/sd-server-${{ matrix.settings.platform }} + + - name: Generate checksum + run: | + cd dist + sha256sum sd-server-${{ matrix.settings.platform }} > sd-server-${{ matrix.settings.platform }}.sha256 + + - name: Create archive + run: | + cd dist + tar -czf sd-server-${{ matrix.settings.platform }}.tar.gz sd-server-${{ matrix.settings.platform }} sd-server-${{ matrix.settings.platform }}.sha256 + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: server-${{ matrix.settings.platform }} + path: dist/sd-server-${{ matrix.settings.platform }}.tar.gz + # V2 Desktop builds desktop-main: strategy: @@ -229,12 +294,12 @@ jobs: if: always() && runner.os == 'macOS' run: security delete-keychain signing_temp.keychain || true - # Create unified release with CLI and Desktop artifacts + # Create unified release with Server, CLI, and Desktop artifacts release: if: startsWith(github.ref, 'refs/tags/') runs-on: self-hosted name: Create Release - needs: [desktop-main] + needs: [server-build, desktop-main] permissions: contents: write steps: @@ -247,8 +312,10 @@ jobs: draft: true files: | cli-*/* + server-*/* */*.dmg */*.msi */*.deb */*.tar.xz + */*.tar.gz */*.sig diff --git a/.github/workflows/server.yml b/.github/workflows/server.yml index 7279d97e2..2c689fdbe 100644 --- a/.github/workflows/server.yml +++ b/.github/workflows/server.yml @@ -1,12 +1,13 @@ name: Server release on: - release: - types: [published] + push: + tags: + - "v*" pull_request: paths: - '.github/workflows/server.yml' - - 'apps/server/docker/*' + - 'apps/server/**' workflow_dispatch: jobs: @@ -81,10 +82,12 @@ jobs: shell: bash run: | set -euxo pipefail - if [ "$GITHUB_EVENT_NAME" == "release" ]; then + if [[ "$GITHUB_REF" == refs/tags/* ]]; then IMAGE_TAG="${GITHUB_REF##*/}" + EXTRA_TAG="latest" else IMAGE_TAG="$(git rev-parse --short "$GITHUB_SHA")" + EXTRA_TAG="staging" fi IMAGE_TAG="${IMAGE_TAG,,}" IMAGE_NAME="${GITHUB_REPOSITORY,,}/server" @@ -92,6 +95,7 @@ jobs: echo "Building ${IMAGE_NAME}:${IMAGE_TAG}" echo "tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" + echo "extra_tag=${EXTRA_TAG}" >> "$GITHUB_OUTPUT" echo "name=${IMAGE_NAME}" >> "$GITHUB_OUTPUT" echo "repo=${GITHUB_REPOSITORY}" >> "$GITHUB_OUTPUT" echo "repo_ref=${GITHUB_HEAD_REF:-${GITHUB_REF#refs/heads/}}" >> "$GITHUB_OUTPUT" @@ -105,16 +109,16 @@ jobs: id: build-image uses: redhat-actions/buildah-build@v2 with: - tags: ${{ steps.image_info.outputs.tag }} ${{ github.event_name == 'release' && 'latest' || 'staging' }} - archs: amd64 + tags: ${{ steps.image_info.outputs.tag }} ${{ steps.image_info.outputs.extra_tag }} + archs: amd64, arm64 image: ${{ steps.image_info.outputs.name }} layers: 'false' - context: ./apps/server/docker + context: . build-args: | REPO=${{ steps.image_info.outputs.repo }} REPO_REF=${{ steps.image_info.outputs.repo_ref }} containerfiles: | - ./apps/server/docker/Dockerfile + ./apps/server/Dockerfile - name: Push image to ghcr.io uses: redhat-actions/push-to-registry@v2 diff --git a/DOCKER.md b/DOCKER.md deleted file mode 100644 index 746e5895c..000000000 --- a/DOCKER.md +++ /dev/null @@ -1,158 +0,0 @@ -# Spacedrive Docker Deployment - -Quick guide for running Spacedrive daemon in Docker. - -## Quick Start - -```bash -# Build the image -docker compose build - -# Start daemon -docker compose up -d - -# View logs -docker compose logs -f - -# Check status -docker exec spacedrive-daemon sd-cli status -``` - -## Supported Platforms - -- **x86_64** (amd64) - Servers, TrueNAS, Intel/AMD systems -- **ARM64** (aarch64) - Raspberry Pi 3/4/5, Apple Silicon (via emulation) - -## Configuration - -Edit `docker-compose.yml` to customize: - -```yaml -volumes: - # Mount directories to index - - /path/to/your/photos:/mnt/photos:ro - - /path/to/your/documents:/mnt/docs:ro - -environment: - # Optional: Set instance name - - SPACEDRIVE_INSTANCE=myserver -``` - -## CLI Access - -```bash -# Run any CLI command -docker exec spacedrive-daemon sd-cli - -# Examples: -docker exec spacedrive-daemon sd-cli library list -docker exec spacedrive-daemon sd-cli location add /mnt/photos -docker exec spacedrive-daemon sd-cli search "vacation" -``` - -## Data Persistence - -Data is stored in the `spacedrive-data` Docker volume. To backup: - -```bash -# Backup volume -docker run --rm -v spacedrive-data:/data -v $(pwd):/backup \ - alpine tar czf /backup/spacedrive-backup.tar.gz /data - -# Restore volume -docker run --rm -v spacedrive-data:/data -v $(pwd):/backup \ - alpine tar xzf /backup/spacedrive-backup.tar.gz -C / -``` - -## Building for Specific Platform - -```bash -# Build for ARM64 (Raspberry Pi) -docker build --platform linux/arm64 -t spacedrive:arm64 . - -# Build for x86_64 -docker build --platform linux/amd64 -t spacedrive:amd64 . -``` - -## TrueNAS Deployment - -1. Enable Apps in TrueNAS SCALE -2. Create custom app using the provided `docker-compose.yml` -3. Mount your pools as volumes: - ```yaml - volumes: - - /mnt/pool1:/mnt/pool1:ro - - /mnt/pool2:/mnt/pool2:ro - ``` - -## Troubleshooting - -### Container exits immediately - -Check logs: -```bash -docker compose logs -``` - -### Can't access daemon - -Verify it's running: -```bash -docker ps -docker exec spacedrive-daemon sd-cli status -``` - -### Out of disk space - -Check Docker disk usage: -```bash -docker system df -``` - -Clean up old data: -```bash -docker system prune -``` - -## Advanced - -### Resource Limits - -Add to `docker-compose.yml`: - -```yaml -deploy: - resources: - limits: - cpus: '2.0' - memory: 2G -``` - -### Custom Data Directory - -```yaml -volumes: - # Use host directory instead of volume - - /path/to/spacedrive/data:/data -``` - -### Network Access (Future API) - -```yaml -ports: - - "8080:8080" # Expose API port -``` - -## Full Documentation - -See [Linux Deployment Guide](./docs/cli/linux-deployment.mdx) for complete documentation including: -- Native binary installation -- Systemd service setup -- Raspberry Pi specific configuration -- TrueNAS integration -- Performance tuning - -## Getting Help - -- Documentation: https://docs.spacedrive.com -- GitHub Issues: https://github.com/spacedriveapp/spacedrive/issues diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 1889a3157..000000000 --- a/Dockerfile +++ /dev/null @@ -1,76 +0,0 @@ -# Multi-stage Dockerfile for Spacedrive CLI and Daemon -# Supports: x86_64 and aarch64 Linux - -# ============================================================================ -# Builder Stage - Compile Rust binaries -# ============================================================================ -FROM rust:1.81-slim-bookworm AS builder - -# Install build dependencies -RUN apt-get update && apt-get install -y \ - build-essential \ - pkg-config \ - libssl-dev \ - git \ - && rm -rf /var/lib/apt/lists/* - -# Set working directory -WORKDIR /build - -# Copy workspace files -COPY Cargo.toml Cargo.lock ./ -COPY apps/ apps/ -COPY core/ core/ -COPY crates/ crates/ -COPY xtask/ xtask/ - -# Copy dependencies (specta, opendal) -COPY specta/ specta/ -COPY opendal/ opendal/ - -# Build release binaries -# Note: We only build CLI features, no FFmpeg or AI models needed -RUN cargo build --release --bin sd-cli --bin sd-daemon - -# ============================================================================ -# Runtime Stage - Minimal image with only runtime dependencies -# ============================================================================ -FROM debian:bookworm-slim - -# Install runtime dependencies -RUN apt-get update && apt-get install -y \ - ca-certificates \ - libssl3 \ - && rm -rf /var/lib/apt/lists/* - -# Create non-root user -RUN useradd -m -u 1000 spacedrive - -# Create data directory -RUN mkdir -p /data && chown spacedrive:spacedrive /data - -# Copy binaries from builder -COPY --from=builder /build/target/release/sd-cli /usr/local/bin/sd-cli -COPY --from=builder /build/target/release/sd-daemon /usr/local/bin/sd-daemon - -# Set permissions -RUN chmod +x /usr/local/bin/sd-cli /usr/local/bin/sd-daemon - -# Switch to non-root user -USER spacedrive - -# Set data directory as volume -VOLUME /data - -# Expose any ports if needed (future: add when API is enabled) -# EXPOSE 8080 - -# Set environment variables -ENV SPACEDRIVE_DATA_DIR=/data - -# Healthcheck -HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \ - CMD sd-cli status || exit 1 - -# Default command: start daemon in foreground -CMD ["sd-cli", "--data-dir", "/data", "start", "--foreground"] diff --git a/README.md b/README.md index 3965b8591..7758b065b 100644 --- a/README.md +++ b/README.md @@ -135,7 +135,7 @@ Peer-to-peer synchronization without central coordinators. Device-specific data **Apps** - **CLI** - Command-line interface (available now) -- **Server** - Headless daemon for Docker deployment (available now) +- **Server** - Headless daemon for Docker deployment ([self-hosting guide](https://v2.spacedrive.com/overview/self-hosting)) - **Tauri** - Cross-platform desktop with React frontend (macOS and Linux now, Windows in alpha.2) - **Web** - Web interface and shared UI components (available now) - **Mobile** - React Native mobile app (iOS and Android coming soon) @@ -331,6 +331,7 @@ Optional cloud integration (Spacedrive Cloud) is available for backup and remote ## Documentation - **[v2 Documentation](https://v2.spacedrive.com)** - Complete guides and API reference +- **[Self-Hosting Guide](https://v2.spacedrive.com/overview/self-hosting)** - Deploy Spacedrive server - **[Whitepaper](whitepaper/spacedrive.pdf)** - Technical architecture (work in progress) - **[Contributing Guide](CONTRIBUTING.md)** - How to contribute - **[Architecture Docs](docs/core/architecture.md)** - Detailed system design diff --git a/SERVER_RELEASE_SETUP.md b/SERVER_RELEASE_SETUP.md new file mode 100644 index 000000000..bba9bcfb8 --- /dev/null +++ b/SERVER_RELEASE_SETUP.md @@ -0,0 +1,302 @@ +# Server Release Setup + +This document explains the changes made to integrate Spacedrive server builds into the release workflow. + +## Overview + +The server app (`sd-server`) is now built and released in two formats: +1. **Static binaries** - For systemd, bare metal, and custom deployments +2. **Docker images** - For containerized deployments (Docker, Kubernetes, NAS systems) + +Both are automatically built and published when a git tag is pushed (e.g., `v2.0.0-alpha.2`). + +**Note:** The root `/Dockerfile` has been removed as it was redundant. The only Docker image for self-hosting is `apps/server/Dockerfile`, which builds the HTTP server with embedded daemon and full media processing support. + +## Changes Made + +### 1. Release Workflow (`.github/workflows/release.yml`) + +**Added: `server-build` job** + +Builds static server binaries for: +- `linux-x86_64` (Intel/AMD servers) +- `linux-aarch64` (ARM servers, Raspberry Pi, AWS Graviton) + +**Features:** +- Full media processing support (`heif`, `ffmpeg` features enabled) +- Cross-compilation for ARM using `gcc-aarch64-linux-gnu` +- Checksums generated for each binary (SHA256) +- Archives created as `.tar.gz` for easy distribution + +**Artifacts uploaded:** +- `sd-server-linux-x86_64.tar.gz` +- `sd-server-linux-aarch64.tar.gz` + +**Updated: `release` job** + +- Added `server-build` to dependencies +- Server artifacts now included in GitHub releases +- Pattern updated to include `.tar.gz` files + +### 2. Server Docker Workflow (`.github/workflows/server.yml`) + +**Changed trigger:** +- Old: `release: types: [published]` +- New: `push: tags: ["v*"]` +- Result: Docker images built at the same time as binaries + +**Multi-arch support:** +- Old: `amd64` only +- New: `amd64` + `arm64` +- Uses QEMU for ARM cross-compilation + +**Fixed paths:** +- Old: `context: ./apps/server/docker` (incorrect) +- New: `context: .` (repo root) +- Old: `containerfiles: ./apps/server/docker/Dockerfile` (incorrect) +- New: `containerfiles: ./apps/server/Dockerfile` (correct) + +**Image tagging:** +- Git tags (e.g., `v2.0.0-alpha.2`) → `ghcr.io/spacedriveapp/spacedrive/server:v2.0.0-alpha.2` + `latest` +- Non-tagged commits → `ghcr.io/spacedriveapp/spacedrive/server:` + `staging` + +### 3. Server Dockerfile (`apps/server/Dockerfile`) + +**Added media processing dependencies:** + +Builder stage: +- `cmake`, `nasm` - Build tools for native dependencies +- `libavcodec-dev`, `libavformat-dev`, `libavutil-dev`, `libswscale-dev` - FFmpeg dev libraries +- `libheif-dev` - HEIF image format support + +Runtime stage: +- Changed from `distroless/cc` to `debian:bookworm-slim` +- Installed runtime libraries: `libavcodec59`, `libavformat59`, `libavutil57`, `libswscale6`, `libheif1` +- Created `spacedrive` user (UID 1000) for security + +**Enabled features in build:** +```dockerfile +cargo build --release -p sd-server --features sd-core/heif,sd-core/ffmpeg +``` + +This enables: +- Video thumbnail generation +- Audio transcription +- HEIF/HEIC image support +- All media processing capabilities + +## Release Process + +### Automated Release (Recommended) + +1. **Tag a release:** + ```bash + git tag v2.0.0-alpha.2 + git push origin v2.0.0-alpha.2 + ``` + +2. **GitHub Actions automatically:** + - Builds server binaries (x86_64 + ARM) + - Builds desktop apps (macOS + Linux) + - Builds Docker images (amd64 + arm64) + - Creates draft GitHub release with all artifacts + +3. **Review and publish:** + - Go to GitHub Releases + - Edit the draft release + - Add release notes + - Publish + +### Manual Testing + +**Test static binary build:** +```bash +# From project root +cargo build --release -p sd-server --features sd-core/heif,sd-core/ffmpeg + +# Test locally +./target/release/sd-server --data-dir /tmp/sd-test +``` + +**Test Docker build:** +```bash +# From project root +docker build -f apps/server/Dockerfile -t sd-server-test . + +# Run locally +docker run -p 8080:8080 -e SD_AUTH=admin:test sd-server-test +``` + +**Test multi-arch Docker build:** +```bash +docker buildx create --use +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -f apps/server/Dockerfile \ + -t sd-server-multiarch \ + . +``` + +## Deployment Options + +### Option 1: Static Binary (systemd) + +```bash +# Download from GitHub release +wget https://github.com/spacedriveapp/spacedrive/releases/download/v2.0.0-alpha.2/sd-server-linux-x86_64.tar.gz +tar -xzf sd-server-linux-x86_64.tar.gz + +# Verify checksum +sha256sum -c sd-server-linux-x86_64.sha256 + +# Install +sudo mv sd-server-linux-x86_64 /usr/local/bin/sd-server +sudo chmod +x /usr/local/bin/sd-server + +# Create systemd service +sudo nano /etc/systemd/system/spacedrive.service +``` + +Example systemd unit: +```ini +[Unit] +Description=Spacedrive Server +After=network.target + +[Service] +Type=simple +User=spacedrive +Environment="DATA_DIR=/var/lib/spacedrive" +Environment="SD_AUTH=admin:your-secure-password" +ExecStart=/usr/local/bin/sd-server --data-dir /var/lib/spacedrive +Restart=on-failure + +[Install] +WantedBy=multi-user.target +``` + +### Option 2: Docker + +```bash +docker run -d \ + --name spacedrive \ + -p 8080:8080 \ + -p 7373:7373 \ + -v spacedrive-data:/data \ + -e SD_AUTH=admin:password \ + ghcr.io/spacedriveapp/spacedrive/server:latest +``` + +### Option 3: Docker Compose + +Use the provided `docker-compose.yml` in `apps/server/`: + +```bash +cd apps/server +docker-compose up -d +``` + +Or create your own: + +```yaml +version: '3.8' +services: + spacedrive: + image: ghcr.io/spacedriveapp/spacedrive/server:latest + ports: + - "8080:8080" + - "7373:7373" + volumes: + - spacedrive-data:/data + - /mnt/storage:/storage:ro # Optional: mount storage + environment: + SD_AUTH: "admin:your-password" + TZ: "America/New_York" + restart: unless-stopped + +volumes: + spacedrive-data: +``` + +## Architecture Support + +| Platform | Binary | Docker | +|----------|--------|--------| +| Linux x86_64 (Intel/AMD) | ✅ | ✅ | +| Linux ARM64 (Raspberry Pi, AWS Graviton) | ✅ | ✅ | +| macOS | ❌ (desktop app only) | ❌ | +| Windows | ❌ (desktop app only) | ❌ | + +## Verify Release Artifacts + +After a release is created, verify these files exist: + +**Server binaries:** +- `sd-server-linux-x86_64.tar.gz` +- `sd-server-linux-aarch64.tar.gz` + +**Desktop apps:** +- `Spacedrive__aarch64.dmg` (macOS ARM) +- `Spacedrive__amd64.deb` (Linux) +- `dist.tar.xz` (frontend assets) + +**Docker images:** +Check ghcr.io: +```bash +docker pull ghcr.io/spacedriveapp/spacedrive/server:v2.0.0-alpha.2 +docker pull ghcr.io/spacedriveapp/spacedrive/server:latest +``` + +## Next Steps + +### Potential Enhancements + +1. **Add Windows server binary** - Build `sd-server.exe` for Windows Server deployments +2. **Package formats** - Create `.deb` and `.rpm` packages for easier installation +3. **ARM macOS** - Server binary for macOS (though desktop app is preferred) +4. **Static linking** - Fully static binaries using `musl` for maximum compatibility +5. **Checksums in release notes** - Auto-generate checksums table in release description + +### Documentation Updates Needed + +- [x] Add "Self-Hosting Guide" to docs (`docs/overview/self-hosting.mdx`) +- [x] Update main README with server deployment links +- [ ] Create TrueNAS app manifest for one-click install +- [ ] Write Unraid template + +## Troubleshooting + +### Build fails with "media features not found" + +The workflow now automatically includes `sd-core/heif` and `sd-core/ffmpeg` features. If this fails: +- Check native dependencies are installed (cmake, nasm, FFmpeg dev packages) +- Verify setup-system action runs successfully + +### Docker image size too large + +Current runtime image uses `debian:bookworm-slim` (~80-100MB base) plus runtime libraries. + +To reduce size: +- Consider Alpine Linux base (smaller but more complex dependencies) +- Use distroless and manually copy .so files (more brittle) +- Current approach prioritizes reliability over size + +### Multi-arch build timeout + +ARM builds can be slow via QEMU emulation. Options: +- Use native ARM runners (more expensive) +- Build in parallel jobs +- Cache build artifacts more aggressively + +### Permission denied in container + +The container runs as user `spacedrive` (UID 1000). If mounting volumes: +```bash +# Fix permissions +sudo chown -R 1000:1000 /path/to/data +``` + +Or run as root (not recommended): +```bash +docker run --user root ... +``` diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile index cdb591e75..3397106e5 100644 --- a/apps/server/Dockerfile +++ b/apps/server/Dockerfile @@ -11,7 +11,14 @@ RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/ git \ pkg-config \ libssl-dev \ - ca-certificates + ca-certificates \ + cmake \ + nasm \ + libavcodec-dev \ + libavformat-dev \ + libavutil-dev \ + libswscale-dev \ + libheif-dev # Install Rust RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal @@ -28,17 +35,32 @@ COPY core ./core COPY crates ./crates COPY apps/server ./apps/server -# Build server (RPC only, no assets) +# Build server with media processing features RUN --mount=type=cache,target=/root/.cargo/registry \ --mount=type=cache,target=/root/.cargo/git \ --mount=type=cache,target=/build/target \ - cargo build --release -p sd-server && \ + cargo build --release -p sd-server --features sd-core/heif,sd-core/ffmpeg && \ cp target/release/sd-server /usr/local/bin/sd-server #-- # Runtime image #-- -FROM gcr.io/distroless/cc-debian12:nonroot +FROM debian:bookworm-slim + +# Install runtime dependencies only +RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \ + apt-get update && apt-get install -y \ + libssl3 \ + ca-certificates \ + libavcodec59 \ + libavformat59 \ + libavutil57 \ + libswscale6 \ + libheif1 \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user +RUN useradd -m -u 1000 spacedrive # Copy binary COPY --from=builder /usr/local/bin/sd-server /usr/bin/sd-server @@ -58,7 +80,7 @@ EXPOSE 7373 VOLUME ["/data"] # Run as non-root user -USER nonroot:nonroot +USER spacedrive:spacedrive # Start server ENTRYPOINT ["/usr/bin/sd-server"] diff --git a/core/src/infra/action/manager.rs b/core/src/infra/action/manager.rs index 79fe7b826..6dbce1e69 100644 --- a/core/src/infra/action/manager.rs +++ b/core/src/infra/action/manager.rs @@ -36,7 +36,7 @@ impl ActionManager { // Check if confirmation is required match validation_result { - super::ValidationResult::Success => { + super::ValidationResult::Success { .. } => { // Proceed with execution } super::ValidationResult::RequiresConfirmation(_request) => { @@ -95,7 +95,7 @@ impl ActionManager { // Check if confirmation is required match validation_result { - super::ValidationResult::Success => { + super::ValidationResult::Success { .. } => { // Proceed with execution } super::ValidationResult::RequiresConfirmation(_request) => { diff --git a/core/src/infra/action/mod.rs b/core/src/infra/action/mod.rs index 8d54e50a0..b2cd9afbb 100644 --- a/core/src/infra/action/mod.rs +++ b/core/src/infra/action/mod.rs @@ -22,7 +22,11 @@ pub mod receipt; #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ValidationResult { /// The action is valid and can proceed without user interaction. - Success, + Success { + /// Optional metadata for rich UI display (strategy info, file counts, etc.) + #[serde(skip_serializing_if = "Option::is_none")] + metadata: Option, + }, /// The action is valid, but requires user confirmation to proceed. RequiresConfirmation(ConfirmationRequest), } @@ -64,7 +68,7 @@ pub trait CoreAction: Send + Sync + 'static { ) -> impl std::future::Future< Output = Result, > + Send { - async { Ok(ValidationResult::Success) } + async { Ok(ValidationResult::Success { metadata: None }) } } /// Resolve a user confirmation choice (optional) @@ -112,7 +116,7 @@ pub trait LibraryAction: Send + Sync + 'static { ) -> impl std::future::Future< Output = Result, > + Send { - async { Ok(ValidationResult::Success) } + async { Ok(ValidationResult::Success { metadata: None }) } } /// Resolve a user confirmation choice (optional) diff --git a/core/src/ops/files/copy/action.rs b/core/src/ops/files/copy/action.rs index 5e32a1d35..a7f683788 100644 --- a/core/src/ops/files/copy/action.rs +++ b/core/src/ops/files/copy/action.rs @@ -321,10 +321,10 @@ impl LibraryAction for FileCopyAction { "strategy": strategy_metadata, "file_count": file_count, "total_bytes": total_bytes, - "conflicts": conflicts.iter().map(|c| { + "conflicts": conflicts.iter().map(|(source, dest)| { json!({ - "source": c.to_string_lossy(), - "destination": c.to_string_lossy(), + "source": source.to_string_lossy(), + "destination": dest.to_string_lossy(), }) }).collect::>(), "is_fast_operation": strategy_metadata.is_fast_operation, @@ -357,7 +357,9 @@ impl LibraryAction for FileCopyAction { // If it's a fast operation with no conflicts, return success with metadata // Frontend can use this to decide whether to show a modal or auto-proceed - Ok(ValidationResult::Success) + Ok(ValidationResult::Success { + metadata: Some(metadata), + }) } fn resolve_confirmation(&mut self, choice_index: usize) -> Result<(), ActionError> { @@ -474,8 +476,8 @@ impl FileCopyAction { Ok((count, size)) } - /// Check for all file conflicts and return list of conflicting paths - async fn check_for_conflicts_detailed(&self) -> Result, ActionError> { + /// Check for all file conflicts and return list of conflicting (source, destination) pairs + async fn check_for_conflicts_detailed(&self) -> Result, ActionError> { let mut conflicts = Vec::new(); let dest_path = match self.destination.as_local_path() { @@ -501,7 +503,7 @@ impl FileCopyAction { // Check if this would conflict if actual_dest.exists() { - conflicts.push(actual_dest); + conflicts.push((source_path.to_path_buf(), actual_dest)); } } } @@ -512,7 +514,7 @@ impl FileCopyAction { /// Check if any destination files would cause conflicts (legacy method) async fn check_for_conflicts(&self) -> Result, ActionError> { let conflicts = self.check_for_conflicts_detailed().await?; - Ok(conflicts.into_iter().next()) + Ok(conflicts.into_iter().next().map(|(_, dest)| dest)) } /// Generate a unique destination path by appending a number if the original exists diff --git a/core/src/ops/files/copy/job.rs b/core/src/ops/files/copy/job.rs index 56a84eb1b..6617b1497 100644 --- a/core/src/ops/files/copy/job.rs +++ b/core/src/ops/files/copy/job.rs @@ -915,6 +915,13 @@ impl ToGenericProgress for CopyProgress { progress = progress.with_current_path(path.clone()); } + // Add strategy metadata for UI display + if let Some(ref strategy_metadata) = self.strategy_metadata { + progress = progress.with_metadata(serde_json::json!({ + "strategy": strategy_metadata + })); + } + progress } } diff --git a/core/src/ops/files/create_folder/action.rs b/core/src/ops/files/create_folder/action.rs index 847edae81..2b70881be 100644 --- a/core/src/ops/files/create_folder/action.rs +++ b/core/src/ops/files/create_folder/action.rs @@ -87,7 +87,7 @@ impl LibraryAction for CreateFolderAction { } } - Ok(ValidationResult::Success) + Ok(ValidationResult::Success { metadata: None }) } async fn execute( diff --git a/core/src/ops/files/delete/action.rs b/core/src/ops/files/delete/action.rs index e3bf0f6de..41110da99 100644 --- a/core/src/ops/files/delete/action.rs +++ b/core/src/ops/files/delete/action.rs @@ -85,7 +85,7 @@ impl LibraryAction for FileDeleteAction { }); } - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/files/rename/action.rs b/core/src/ops/files/rename/action.rs index 31168fbd4..e8871cd61 100644 --- a/core/src/ops/files/rename/action.rs +++ b/core/src/ops/files/rename/action.rs @@ -72,7 +72,7 @@ impl LibraryAction for FileRenameAction { _ => {} } - Ok(ValidationResult::Success) + Ok(ValidationResult::Success { metadata: None }) } async fn execute( diff --git a/core/src/ops/indexing/action.rs b/core/src/ops/indexing/action.rs index 428e324c5..e14f7e459 100644 --- a/core/src/ops/indexing/action.rs +++ b/core/src/ops/indexing/action.rs @@ -75,7 +75,7 @@ impl LibraryAction for IndexingAction { message: errors.join("; "), }); } - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } async fn execute( diff --git a/core/src/ops/libraries/create/action.rs b/core/src/ops/libraries/create/action.rs index 5cb546798..eb89c1463 100644 --- a/core/src/ops/libraries/create/action.rs +++ b/core/src/ops/libraries/create/action.rs @@ -77,7 +77,7 @@ impl CoreAction for LibraryCreateAction { message: "Library name cannot be empty".to_string(), }); } - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/libraries/open/action.rs b/core/src/ops/libraries/open/action.rs index d61783631..e7c61ac85 100644 --- a/core/src/ops/libraries/open/action.rs +++ b/core/src/ops/libraries/open/action.rs @@ -118,7 +118,7 @@ impl CoreAction for LibraryOpenAction { }); } - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/libraries/rename/action.rs b/core/src/ops/libraries/rename/action.rs index 2bfa7d303..9adc406f8 100644 --- a/core/src/ops/libraries/rename/action.rs +++ b/core/src/ops/libraries/rename/action.rs @@ -87,7 +87,7 @@ impl LibraryAction for LibraryRenameAction { }); } - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/locations/add/action.rs b/core/src/ops/locations/add/action.rs index 8f98dd2b6..f77b09df1 100644 --- a/core/src/ops/locations/add/action.rs +++ b/core/src/ops/locations/add/action.rs @@ -191,7 +191,7 @@ impl LibraryAction for LocationAddAction { // Check for duplicate locations // TODO: Implement proper duplicate detection for both Physical and Cloud paths - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/locations/enable_indexing/action.rs b/core/src/ops/locations/enable_indexing/action.rs index 3867d27da..7f0bda45d 100644 --- a/core/src/ops/locations/enable_indexing/action.rs +++ b/core/src/ops/locations/enable_indexing/action.rs @@ -191,7 +191,7 @@ impl LibraryAction for EnableIndexingAction { }); } - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/locations/import/action.rs b/core/src/ops/locations/import/action.rs index 8ee57c9be..4d8c77006 100644 --- a/core/src/ops/locations/import/action.rs +++ b/core/src/ops/locations/import/action.rs @@ -58,7 +58,7 @@ impl LibraryAction for LocationImportAction { }); } - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } async fn execute( diff --git a/core/src/ops/locations/trigger_job/action.rs b/core/src/ops/locations/trigger_job/action.rs index 5f95e61b4..d9fc34e05 100644 --- a/core/src/ops/locations/trigger_job/action.rs +++ b/core/src/ops/locations/trigger_job/action.rs @@ -235,7 +235,7 @@ impl LibraryAction for LocationTriggerJobAction { return Err(ActionError::LocationNotFound(self.input.location_id)); } - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/locations/update/action.rs b/core/src/ops/locations/update/action.rs index 0e7aebc54..465dfeb77 100644 --- a/core/src/ops/locations/update/action.rs +++ b/core/src/ops/locations/update/action.rs @@ -115,7 +115,7 @@ impl LibraryAction for LocationUpdateAction { return Err(ActionError::LocationNotFound(self.input.id)); } - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/network/sync_setup/action.rs b/core/src/ops/network/sync_setup/action.rs index 92447ca47..83bf611f8 100644 --- a/core/src/ops/network/sync_setup/action.rs +++ b/core/src/ops/network/sync_setup/action.rs @@ -109,7 +109,7 @@ impl CoreAction for LibrarySyncSetupAction { }); } - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } @@ -221,7 +221,7 @@ impl LibrarySyncSetupAction { ); } - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } /// Execute ShareLocalLibrary action - share local library to remote device diff --git a/core/src/ops/spaces/add_group/action.rs b/core/src/ops/spaces/add_group/action.rs index 4549ed7e6..534281dfe 100644 --- a/core/src/ops/spaces/add_group/action.rs +++ b/core/src/ops/spaces/add_group/action.rs @@ -113,7 +113,7 @@ impl LibraryAction for AddGroupAction { _library: &std::sync::Arc, _context: std::sync::Arc, ) -> Result { - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/spaces/add_item/action.rs b/core/src/ops/spaces/add_item/action.rs index 3f66bda6e..938e862f6 100644 --- a/core/src/ops/spaces/add_item/action.rs +++ b/core/src/ops/spaces/add_item/action.rs @@ -148,7 +148,7 @@ impl LibraryAction for AddItemAction { _library: &std::sync::Arc, _context: std::sync::Arc, ) -> Result { - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/spaces/create/action.rs b/core/src/ops/spaces/create/action.rs index ed48fef3c..1d04da328 100644 --- a/core/src/ops/spaces/create/action.rs +++ b/core/src/ops/spaces/create/action.rs @@ -110,7 +110,7 @@ impl LibraryAction for SpaceCreateAction { _library: &std::sync::Arc, _context: std::sync::Arc, ) -> Result { - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/spaces/delete/action.rs b/core/src/ops/spaces/delete/action.rs index 5322b0ce6..0d1e13cc8 100644 --- a/core/src/ops/spaces/delete/action.rs +++ b/core/src/ops/spaces/delete/action.rs @@ -59,7 +59,7 @@ impl LibraryAction for SpaceDeleteAction { _library: &std::sync::Arc, _context: std::sync::Arc, ) -> Result { - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/spaces/delete_group/action.rs b/core/src/ops/spaces/delete_group/action.rs index e5f19c41a..4275cd19c 100644 --- a/core/src/ops/spaces/delete_group/action.rs +++ b/core/src/ops/spaces/delete_group/action.rs @@ -69,7 +69,7 @@ impl LibraryAction for DeleteGroupAction { _library: &std::sync::Arc, _context: std::sync::Arc, ) -> Result { - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/spaces/delete_item/action.rs b/core/src/ops/spaces/delete_item/action.rs index 7d4b2624c..dfebcc67f 100644 --- a/core/src/ops/spaces/delete_item/action.rs +++ b/core/src/ops/spaces/delete_item/action.rs @@ -68,7 +68,7 @@ impl LibraryAction for DeleteItemAction { _library: &std::sync::Arc, _context: std::sync::Arc, ) -> Result { - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/spaces/reorder/action.rs b/core/src/ops/spaces/reorder/action.rs index 1bfc8d732..d6fdc68a5 100644 --- a/core/src/ops/spaces/reorder/action.rs +++ b/core/src/ops/spaces/reorder/action.rs @@ -59,7 +59,7 @@ impl LibraryAction for ReorderGroupsAction { _library: &std::sync::Arc, _context: std::sync::Arc, ) -> Result { - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } @@ -110,7 +110,7 @@ impl LibraryAction for ReorderItemsAction { _library: &std::sync::Arc, _context: std::sync::Arc, ) -> Result { - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/spaces/update/action.rs b/core/src/ops/spaces/update/action.rs index 3b4b0c83c..38225be6a 100644 --- a/core/src/ops/spaces/update/action.rs +++ b/core/src/ops/spaces/update/action.rs @@ -108,7 +108,7 @@ impl LibraryAction for SpaceUpdateAction { _library: &std::sync::Arc, _context: std::sync::Arc, ) -> Result { - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/core/src/ops/spaces/update_group/action.rs b/core/src/ops/spaces/update_group/action.rs index b6097e961..c911c87db 100644 --- a/core/src/ops/spaces/update_group/action.rs +++ b/core/src/ops/spaces/update_group/action.rs @@ -107,7 +107,7 @@ impl LibraryAction for UpdateGroupAction { _library: &std::sync::Arc, _context: std::sync::Arc, ) -> Result { - Ok(crate::infra::action::ValidationResult::Success) + Ok(crate::infra::action::ValidationResult::Success { metadata: None }) } } diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index e400b666f..000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,38 +0,0 @@ -version: '3.8' - -services: - spacedrive: - build: - context: . - dockerfile: Dockerfile - image: spacedrive:latest - container_name: spacedrive-daemon - restart: unless-stopped - - # Data persistence - volumes: - - spacedrive-data:/data - # Optional: mount host directories to index - # - /path/to/your/files:/mnt/files:ro - - # Environment variables - environment: - - SPACEDRIVE_DATA_DIR=/data - # Optional: Set instance name - # - SPACEDRIVE_INSTANCE=default - - # Optional: Expose ports when API is enabled - # ports: - # - "8080:8080" - - # Health check - healthcheck: - test: ["CMD", "sd-cli", "status"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 10s - -volumes: - spacedrive-data: - driver: local diff --git a/docs/mint.json b/docs/mint.json index 677c0baf3..baa27ec25 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -57,6 +57,7 @@ "icon": "compass", "pages": [ "overview/get-started", + "overview/self-hosting", "overview/backup-photos-ios", "overview/manage-libraries", "overview/add-index-locations" diff --git a/docs/overview/self-hosting.mdx b/docs/overview/self-hosting.mdx new file mode 100644 index 000000000..00efac13c --- /dev/null +++ b/docs/overview/self-hosting.mdx @@ -0,0 +1,317 @@ +--- +title: Self-Hosting Spacedrive +description: Deploy Spacedrive server for remote access and headless operation +sidebarTitle: Self-Hosting +--- + +Self-hosting Spacedrive means running the server component on hardware you control. The server provides HTTP access to your libraries, enabling remote connections from desktop and mobile apps. Unlike the desktop app which requires a GUI, the server runs headless on Linux systems, NAS devices, or cloud instances. + +The server embeds the full Spacedrive daemon with media processing capabilities. It exposes RPC endpoints over HTTP with optional basic authentication. Your data stays on your infrastructure while remaining accessible from any device with network access. + +## Installation Methods + +### Static Binary + +Download the latest release binary for your platform. This method works on any Linux system and requires no container runtime. + +```bash +# Download and extract +wget https://github.com/spacedriveapp/spacedrive/releases/latest/download/sd-server-linux-x86_64.tar.gz +tar -xzf sd-server-linux-x86_64.tar.gz + +# Verify checksum +sha256sum -c sd-server-linux-x86_64.sha256 + +# Install to system +sudo mv sd-server-linux-x86_64 /usr/local/bin/sd-server +sudo chmod +x /usr/local/bin/sd-server +``` + +For ARM systems like Raspberry Pi, use `sd-server-linux-aarch64.tar.gz` instead. + +### Docker Deployment + +Docker images are available for both x86_64 and ARM64 architectures. The image includes all media processing dependencies and runs as a non-root user. + +```bash +docker run -d \ + --name spacedrive \ + -p 8080:8080 \ + -p 7373:7373 \ + -v spacedrive-data:/data \ + -e SD_AUTH=admin:your-secure-password \ + ghcr.io/spacedriveapp/spacedrive/server:latest +``` + +The server listens on port 8080 for HTTP traffic and port 7373 for peer-to-peer connections. Mount additional volumes to index existing storage. + +### Docker Compose + +For production deployments, use the provided compose file in the repository at `apps/server/docker-compose.yml`. This includes health checks and restart policies. + +```yaml +version: '3.8' +services: + spacedrive: + image: ghcr.io/spacedriveapp/spacedrive/server:latest + ports: + - "8080:8080" + - "7373:7373" + volumes: + - spacedrive-data:/data + - /mnt/media:/media:ro + environment: + SD_AUTH: "admin:your-password" + TZ: "America/New_York" + restart: unless-stopped + +volumes: + spacedrive-data: +``` + +## System Service Setup + +Running the server as a systemd service ensures it starts on boot and restarts on failure. + +Create a service file at `/etc/systemd/system/spacedrive.service`: + +```ini +[Unit] +Description=Spacedrive Server +After=network.target + +[Service] +Type=simple +User=spacedrive +Group=spacedrive +Environment="DATA_DIR=/var/lib/spacedrive" +Environment="SD_AUTH=admin:your-secure-password" +ExecStart=/usr/local/bin/sd-server --data-dir /var/lib/spacedrive +Restart=on-failure +RestartSec=5s + +[Install] +WantedBy=multi-user.target +``` + +Create the user and data directory, then enable the service: + +```bash +sudo useradd -r -s /bin/false spacedrive +sudo mkdir -p /var/lib/spacedrive +sudo chown spacedrive:spacedrive /var/lib/spacedrive +sudo systemctl enable --now spacedrive +``` + +Check status with `sudo systemctl status spacedrive`. + +## Configuration + +The server accepts configuration via environment variables or command-line flags. + +**Authentication** is controlled by `SD_AUTH`. The format supports multiple users separated by commas: `user1:pass1,user2:pass2`. Set to `disabled` to disable authentication, though this is not recommended for network-accessible deployments. + +**Data directory** defaults to `/data` in Docker or a temporary directory in development. In production, always set `DATA_DIR` to a persistent location. The server creates library databases, thumbnails, and logs in this directory. + +**Port configuration** defaults to 8080. Change with `--port` flag or `PORT` environment variable. The P2P port (7373) is fixed and required for device sync. + + +Production deployments must set `SD_AUTH`. The server refuses to start without authentication unless explicitly disabled. This prevents accidental exposure of your libraries. + + +## Network Access + +The server binds to all interfaces by default, making it accessible on your local network. For internet access, place it behind a reverse proxy with TLS termination. + +### Nginx Example + +```nginx +server { + listen 443 ssl http2; + server_name spacedrive.example.com; + + ssl_certificate /etc/letsencrypt/live/spacedrive.example.com/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/spacedrive.example.com/privkey.pem; + + location / { + proxy_pass http://localhost:8080; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} +``` + +### Caddy Example + +Caddy handles TLS automatically with Let's Encrypt: + +``` +spacedrive.example.com { + reverse_proxy localhost:8080 +} +``` + +## NAS Deployment + +Network-attached storage devices are ideal for Spacedrive server. The server indexes existing media while providing remote access. + +### TrueNAS SCALE + +Install via the Apps interface using Docker. Map your pools as read-only volumes to prevent accidental modification during indexing. + +```yaml +volumes: + - /mnt/pool1/photos:/photos:ro + - /mnt/pool1/videos:/videos:ro + - /mnt/pool2/documents:/documents:ro +``` + +After deployment, create locations in Spacedrive pointing to these mount points. The server indexes files without moving them. + +### Synology NAS + +Use Container Manager to deploy the Docker image. Configure port forwarding and volume mounts through the UI. Ensure the container has read access to your shared folders. + +### Unraid + +Add a custom container template with the image `ghcr.io/spacedriveapp/spacedrive/server:latest`. Map your shares as additional paths in the container configuration. + +## Storage Considerations + +The server creates three types of data in the data directory: + +**Libraries** contain SQLite databases with file indexes, tags, and metadata. Each library is a separate `.sdlibrary` directory. These are small, typically under 100MB even for millions of files. + +**Sidecars** include thumbnails, previews, and extracted text. Size depends on media processing settings. Budget 1-5% of your total media size for thumbnails. + +**Logs** rotate automatically but can grow large during initial indexing. The server keeps the last 10 log files, typically under 100MB total. + +Plan for library growth over time. A library indexing 1 million files uses approximately 500MB for the database and 10-50GB for sidecars, depending on media density. + + +The server never modifies your original files. Indexing is read-only. File operations like copy and move require explicit user action through connected clients. + + +## Security Best Practices + +Self-hosted servers face different threats than desktop apps. Follow these guidelines for secure deployment. + +**Always use authentication** when the server is network-accessible. HTTP basic auth is sufficient for home networks but consider stronger methods for internet-facing deployments. + +**Enable TLS** for any internet access. Let's Encrypt provides free certificates through Caddy, Nginx, or Traefik. The server itself does not handle TLS, rely on reverse proxies. + +**Limit exposure** with firewall rules. Only open port 8080 to trusted networks or VPN connections. The P2P port (7373) requires UDP ingress for device sync but can be restricted to known peers. + +**Run as non-root** user. The Docker image and systemd example both use dedicated users with minimal permissions. Never run the server as root. + +**Regular backups** of the data directory protect against corruption. The SQLite databases can be backed up while running using the `.backup` command or by copying the entire data directory when the server is stopped. + +## Connecting Clients + +Desktop and mobile apps can connect to self-hosted servers by configuring the server URL in settings. + +The URL format is `http://your-server:8080` or `https://spacedrive.example.com` if using a reverse proxy. Clients authenticate using credentials set in `SD_AUTH`. + +P2P sync works between the server and other devices without additional configuration. The server appears as a device in your library and participates in sync like any other device. + +## Monitoring + +The server provides a health check endpoint at `/health` that returns HTTP 200 when operational. Use this for monitoring tools or container orchestration health probes. + +Logs are written to stdout and the data directory under `logs/`. Set `RUST_LOG` environment variable to `debug` for detailed output during troubleshooting. + +**Key metrics to monitor:** + +Disk usage in the data directory grows with library size. Alert when free space drops below 20% to prevent database corruption. + +Memory usage typically ranges from 100-500MB depending on active jobs. Spike during intensive operations like thumbnail generation. + +Network bandwidth peaks during initial sync with new devices. Ongoing sync uses minimal bandwidth after the first synchronization. + +## Troubleshooting + +**Server won't start:** + +Check that the data directory exists and is writable by the server user. Verify no other process is using port 8080 with `lsof -i :8080`. Review logs for specific errors. + +**Authentication failures:** + +Ensure `SD_AUTH` format is correct. Test credentials with curl: `curl -u admin:password http://localhost:8080/health`. Browser authentication dialogs require exact username and password. + +**Clients can't connect:** + +Verify the server is accessible from the client network. Test with `curl http://server-ip:8080/health`. Check firewall rules on both server and client networks. Ensure reverse proxy configuration preserves authentication headers. + +**Media processing fails:** + +The server requires FFmpeg and libheif for video thumbnails and HEIF images. Docker images include these by default. Binary installations on minimal systems may need manual installation of these libraries. + +**Sync not working:** + +UDP port 7373 must be accessible for P2P connections. Check NAT and firewall rules. The server attempts hole-punching for NAT traversal but may fall back to relay servers if direct connection fails. + +## Performance Tuning + +The server handles multiple concurrent operations through the job system. Performance depends on storage speed, available memory, and indexing load. + +**Indexing performance** is limited by storage I/O. SSDs provide 10-50x faster indexing than spinning disks. Network storage over gigabit ethernet typically indexes at 50-100MB/s for metadata reads. + +**Thumbnail generation** is CPU-bound. Disable thumbnail generation for video files if CPU usage is a concern. Image thumbnails are fast but video processing can use significant resources. + +**Database optimization** happens automatically through SQLite's auto-vacuum and WAL mode. Large libraries benefit from periodic `VACUUM` operations when the server is idle. + +**Memory allocation** can be limited using systemd or Docker resource constraints. The server operates efficiently with 512MB minimum, though 2GB provides better performance for large libraries. + +## Backup Strategy + +Back up the data directory regularly to prevent data loss. The recommended approach depends on your infrastructure. + +**Systemd deployments** can use rsync or similar tools while the server is running. SQLite WAL mode ensures consistent reads during backup. + +**Docker deployments** should back up named volumes. Stop the container, copy the volume contents, then restart. Alternatively, use volume backup tools that handle running containers. + +**Cloud backups** of library databases are small enough for frequent uploads. Sidecar data can be regenerated from source files if needed, reducing backup size. + +Test restore procedures periodically. A backup is only useful if you can restore it successfully. + +## Migration + +Moving an existing library to a new server requires copying the data directory and adjusting location paths. + +Copy the entire data directory to the new server. Start the server and verify the library loads. Update location paths if the mount points differ on the new system. + +File content identity ensures files are recognized even if paths change. The server re-indexes locations with new paths but preserves tags, collections, and metadata. + +## Updates + +The server receives updates through new Docker image tags or binary releases. Always review release notes before updating to understand breaking changes. + +**Docker updates:** + +Pull the new image and recreate the container. Data persists in volumes across container replacements. + +```bash +docker pull ghcr.io/spacedriveapp/spacedrive/server:latest +docker stop spacedrive +docker rm spacedrive +# Run docker run command with new image +``` + +**Binary updates:** + +Download the new release, verify checksums, and replace the binary. Restart the systemd service. + +```bash +sudo systemctl stop spacedrive +sudo mv sd-server-new /usr/local/bin/sd-server +sudo systemctl start spacedrive +``` + +Database migrations run automatically on first start after update. Check logs to confirm successful migration. + +## Support + +Report issues on [GitHub](https://github.com/spacedriveapp/spacedrive/issues) with server logs and deployment details. Join [Discord](https://discord.gg/gTaF2Z44f5) for community support and deployment advice. diff --git a/packages/interface/src/components/JobManager/JobManagerPopover.tsx b/packages/interface/src/components/JobManager/JobManagerPopover.tsx index 5b00ebf18..c0fe68717 100644 --- a/packages/interface/src/components/JobManager/JobManagerPopover.tsx +++ b/packages/interface/src/components/JobManager/JobManagerPopover.tsx @@ -98,6 +98,7 @@ export function JobManagerPopover({ className }: JobManagerPopoverProps) { pause={pause} resume={resume} cancel={cancel} + getSpeedHistory={getSpeedHistory} /> )} @@ -111,6 +112,7 @@ function JobManagerPopoverContent({ pause, resume, cancel, + getSpeedHistory, }: { jobs: any[]; showOnlyRunning: boolean; @@ -118,6 +120,7 @@ function JobManagerPopoverContent({ pause: (jobId: string) => Promise; resume: (jobId: string) => Promise; cancel: (jobId: string) => Promise; + getSpeedHistory: (jobId: string) => import("./hooks/useJobs").SpeedSample[]; }) { const filteredJobs = showOnlyRunning ? jobs.filter((job) => job.status === "running" || job.status === "paused") diff --git a/packages/interface/src/components/JobManager/components/JobCard.tsx b/packages/interface/src/components/JobManager/components/JobCard.tsx index 9f47aaa6b..cc1d4afa1 100644 --- a/packages/interface/src/components/JobManager/components/JobCard.tsx +++ b/packages/interface/src/components/JobManager/components/JobCard.tsx @@ -19,7 +19,7 @@ interface JobCardProps { onPause?: (jobId: string) => void; onResume?: (jobId: string) => void; onCancel?: (jobId: string) => void; - getSpeedHistory: (jobId: string) => SpeedSample[]; + getSpeedHistory?: (jobId: string) => SpeedSample[]; } export function JobCard({ job, onPause, onResume, onCancel, getSpeedHistory }: JobCardProps) { @@ -143,7 +143,7 @@ export function JobCard({ job, onPause, onResume, onCancel, getSpeedHistory }: J {/* Expanded details section */} - {isExpanded && isCopyJob && ( + {isExpanded && isCopyJob && getSpeedHistory && ( j.status === "running").length; const pausedCount = jobs.filter((j) => j.status === "paused").length; - // Helper to get speed history for a job - const getSpeedHistory = (jobId: string): SpeedSample[] => { + // Helper to get speed history for a job (memoized to prevent re-creation) + const getSpeedHistory = useCallback((jobId: string): SpeedSample[] => { return speedHistoryRef.current.get(jobId) || []; - }; + }, []); return { jobs, diff --git a/packages/interface/src/routes/overview/DevicePanel.tsx b/packages/interface/src/routes/overview/DevicePanel.tsx index ea56749aa..c82c5ed60 100644 --- a/packages/interface/src/routes/overview/DevicePanel.tsx +++ b/packages/interface/src/routes/overview/DevicePanel.tsx @@ -286,7 +286,7 @@ function DeviceCard({ }: DeviceCardProps) { const deviceName = device?.name || "Unknown Device"; const deviceIconSrc = device ? getDeviceIcon(device) : null; - const { pause, resume } = useJobs(); + const { pause, resume, getSpeedHistory } = useJobs(); // Format hardware specs const cpuInfo = device?.cpu_model @@ -386,6 +386,7 @@ function DeviceCard({ job={job} onPause={pause} onResume={resume} + getSpeedHistory={getSpeedHistory} /> ))}