diff --git a/.env.example b/.env.example index 172b887..7d09efe 100644 --- a/.env.example +++ b/.env.example @@ -2,5 +2,13 @@ DATABASE_URL=postgres://USER@localhost:5432/stl_library PORT=3000 +# Docker only: where data lives on the host. Both default to ./data beside +# docker-compose.yml. DATA_DIR takes the model files, thumbnails and previews. +DATA_DIR=/Volumes/Data/printhive +# The database is set separately, because Postgres needs a filesystem with real +# ownership and permissions — leave it on local disk if DATA_DIR is exFAT, FAT +# or a network share. +PGDATA_DIR=/Volumes/Data/printhive/pgdata + # Thingiverse import — get a free app token at https://www.thingiverse.com/developers THINGIVERSE_TOKEN= diff --git a/README.md b/README.md index d35c184..36fcc64 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,29 @@ A self-hosted web app for organizing your 3D-printing files (STL, 3MF, OBJ, FBX, Run the unit tests with `npm test`. +### Where your data lives + +With Docker, everything sits in folders on the host — `./data` beside `docker-compose.yml` unless `.env` says +otherwise (`DATA_DIR` for the files, `PGDATA_DIR` for the database): + +| Folder | Holds | +|---|---| +| `$PGDATA_DIR` | The database: models, tags, collections, watches, spools, printers, settings (tokens and printer access codes live here) | +| `$DATA_DIR/uploads` | Your model files, as uploaded or downloaded | +| `$DATA_DIR/thumbnails` | Generated pictures, including plate images | +| `$DATA_DIR/glb` | Converted 3D previews for the viewer | +| `$DATA_DIR/extracted` | Files unpacked from archives — a cache | + +The database is set apart from the rest because Postgres needs a filesystem with real ownership and permissions — +ext4, xfs, APFS and the like. exFAT, FAT and most network shares won't do, so keep `PGDATA_DIR` on local disk if the +files live on one of those. + +A backup needs the database (or a `pg_dump`) and `uploads`; the other three rebuild themselves. Everything else in +the container is replaced on each update, so nothing outside these folders is kept. + +Upgrading from a version that used Docker named volumes: stop the stack, run `tools/volumes-to-folders.sh` to copy the +data across, then start it again. The script only copies — the old volumes stay until you remove them yourself. + ## Configuration Settings live in the database (editable in the app's **Settings** menu) and fall back to environment variables: diff --git a/docker-compose.yml b/docker-compose.yml index 1ae9204..1c34ddd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,11 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-printhive} POSTGRES_DB: printhive volumes: - - pgdata:/var/lib/postgresql/data + # Set separately from DATA_DIR: Postgres needs a filesystem with real + # ownership and permissions (ext4, xfs, APFS...), which exFAT, FAT and + # most network shares don't provide. Keep it on local disk if the files + # live somewhere like that. + - ${PGDATA_DIR:-./data/pgdata}:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U printhive -d printhive"] interval: 5s @@ -33,10 +37,12 @@ services: MAKERWORLD_TOKEN: ${MAKERWORLD_TOKEN:-} PRINTABLES_TOKEN: ${PRINTABLES_TOKEN:-} volumes: - - uploads:/app/uploads - - thumbnails:/app/thumbnails - - glb:/app/glb - - extracted:/app/extracted # cache of files unpacked from archives + # Folders on the host: ./data unless DATA_DIR says otherwise. + # uploads and pgdata hold everything that matters; the other two rebuild. + - ${DATA_DIR:-./data}/uploads:/app/uploads + - ${DATA_DIR:-./data}/thumbnails:/app/thumbnails + - ${DATA_DIR:-./data}/glb:/app/glb + - ${DATA_DIR:-./data}/extracted:/app/extracted # cache of files unpacked from archives # No host port published — Traefik auto-discovers the app on the shared network. networks: - default # talk to the db @@ -52,13 +58,6 @@ services: - "traefik.http.routers.printhive.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}" - "traefik.http.services.printhive.loadbalancer.server.port=3000" -volumes: - pgdata: - uploads: - thumbnails: - glb: - extracted: - networks: default: # The existing Traefik network (the `proxy` network Traefik runs on). diff --git a/tools/volumes-to-folders.sh b/tools/volumes-to-folders.sh new file mode 100755 index 0000000..ab9b55f --- /dev/null +++ b/tools/volumes-to-folders.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env bash +# Copies Printhive's data out of Docker's named volumes into ./data, for the +# switch to host folders. Run it on the server, from the folder holding +# docker-compose.yml, with the stack stopped. +# +# It only copies: the named volumes are left exactly as they were, so if +# anything looks wrong you can put the old docker-compose.yml back and start +# again with nothing lost. +set -euo pipefail + +# Same settings Compose uses, so the copy lands where the stack will look. +if [ -f .env ]; then + DATA_DIR="${DATA_DIR:-$(grep -E '^DATA_DIR=' .env | tail -1 | cut -d= -f2-)}" + PGDATA_DIR="${PGDATA_DIR:-$(grep -E '^PGDATA_DIR=' .env | tail -1 | cut -d= -f2-)}" +fi +DATA_DIR="${DATA_DIR:-./data}" +PGDATA_DIR="${PGDATA_DIR:-./data/pgdata}" +VOLUMES=(pgdata uploads thumbnails glb extracted) + +# Where each volume's contents should end up. +target_for() { + case "$1" in + pgdata) echo "$PGDATA_DIR" ;; + *) echo "$DATA_DIR/$1" ;; + esac +} + +# Compose names volumes "_", the project being this folder's name +# unless it's set elsewhere. +PROJECT="${COMPOSE_PROJECT_NAME:-$(basename "$(pwd)")}" + +if docker compose ps --status running --quiet 2>/dev/null | grep -q .; then + echo "Printhive is still running. Stop it first: docker compose down" >&2 + exit 1 +fi + +echo "Project: $PROJECT" +echo "Files: $DATA_DIR" +echo "Database: $PGDATA_DIR" +echo + +for name in "${VOLUMES[@]}"; do + volume="${PROJECT}_${name}" + target="$(target_for "$name")" + + if ! docker volume inspect "$volume" >/dev/null 2>&1; then + echo "· $volume — no such volume, skipping (nothing to copy)" + mkdir -p "$target" + continue + fi + if [ -d "$target" ] && [ -n "$(ls -A "$target" 2>/dev/null)" ]; then + echo "· $target already has files in it — skipping, so nothing is overwritten" >&2 + continue + fi + + mkdir -p "$target" + # -a keeps ownership and timestamps, which Postgres's data folder needs. + docker run --rm \ + -v "$volume":/from:ro \ + -v "$(cd "$(dirname "$target")" && pwd)/$(basename "$target")":/to \ + alpine sh -c 'cp -a /from/. /to/' + echo "· $volume → $target ($(du -sh "$target" | cut -f1))" +done + +echo +echo "Done. Start it up again with: docker compose up -d" +echo "Once you're happy, the old volumes can be removed with:" +for name in "${VOLUMES[@]}"; do echo " docker volume rm ${PROJECT}_${name}"; done