From 985b81d1deefcf5c328af847945493d473b4478a Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Sun, 20 Sep 2026 15:58:28 +1000 Subject: [PATCH 1/2] Keep data in host folders instead of Docker named volumes The database and files now live under ./data (DATA_DIR to move them), so they can be seen, backed up and restored without going through Docker. tools/volumes-to-folders.sh copies an existing install's named volumes across; it only copies, leaving the volumes in place. Co-Authored-By: Claude Opus 5 --- README.md | 18 +++++++++++++ docker-compose.yml | 19 +++++-------- tools/volumes-to-folders.sh | 53 +++++++++++++++++++++++++++++++++++++ 3 files changed, 78 insertions(+), 12 deletions(-) create mode 100755 tools/volumes-to-folders.sh diff --git a/README.md b/README.md index d35c184..edfb6a7 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,24 @@ 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 next to `docker-compose.yml` (set `DATA_DIR` to put them elsewhere): + +| Folder | Holds | +|---|---| +| `data/pgdata` | The database: models, tags, collections, watches, spools, printers, settings (tokens and printer access codes live here) | +| `data/uploads` | Your model files, as uploaded or downloaded | +| `data/thumbnails` | Generated pictures, including plate images | +| `data/glb` | Converted 3D previews for the viewer | +| `data/extracted` | Files unpacked from archives — a cache | + +A backup needs `data/pgdata` (or a `pg_dump`) and `data/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..968d491 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,7 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-printhive} POSTGRES_DB: printhive volumes: - - pgdata:/var/lib/postgresql/data + - ${DATA_DIR:-./data}/pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U printhive -d printhive"] interval: 5s @@ -33,10 +33,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, under ./data (set DATA_DIR to put them elsewhere). + # 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 +54,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..cbef307 --- /dev/null +++ b/tools/volumes-to-folders.sh @@ -0,0 +1,53 @@ +#!/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 + +DATA_DIR="${DATA_DIR:-./data}" +VOLUMES=(pgdata uploads thumbnails glb extracted) + +# 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 "Copying into: $DATA_DIR" +echo + +for name in "${VOLUMES[@]}"; do + volume="${PROJECT}_${name}" + target="$DATA_DIR/$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 From 6c10cf6b3775aba854099070fff324cc4a0cca2b Mon Sep 17 00:00:00 2001 From: dlawler489 <104159223@student.swin.edu.au> Date: Sun, 20 Sep 2026 16:01:43 +1000 Subject: [PATCH 2/2] Let the data folders be set per install, database separately DATA_DIR takes the files and PGDATA_DIR the database, so the database can stay on a filesystem that supports ownership when the files live on a disk that doesn't. The example .env points both at /Volumes/Data/printhive. Co-Authored-By: Claude Opus 5 --- .env.example | 8 ++++++++ README.md | 19 ++++++++++++------- docker-compose.yml | 8 ++++++-- tools/volumes-to-folders.sh | 21 ++++++++++++++++++--- 4 files changed, 44 insertions(+), 12 deletions(-) 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 edfb6a7..36fcc64 100644 --- a/README.md +++ b/README.md @@ -59,17 +59,22 @@ Run the unit tests with `npm test`. ### Where your data lives -With Docker, everything sits in folders next to `docker-compose.yml` (set `DATA_DIR` to put them elsewhere): +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 | |---|---| -| `data/pgdata` | The database: models, tags, collections, watches, spools, printers, settings (tokens and printer access codes live here) | -| `data/uploads` | Your model files, as uploaded or downloaded | -| `data/thumbnails` | Generated pictures, including plate images | -| `data/glb` | Converted 3D previews for the viewer | -| `data/extracted` | Files unpacked from archives — a cache | +| `$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 | -A backup needs `data/pgdata` (or a `pg_dump`) and `data/uploads`; the other three rebuild themselves. Everything else in +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 diff --git a/docker-compose.yml b/docker-compose.yml index 968d491..1c34ddd 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,7 +6,11 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-printhive} POSTGRES_DB: printhive volumes: - - ${DATA_DIR:-./data}/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,7 +37,7 @@ services: MAKERWORLD_TOKEN: ${MAKERWORLD_TOKEN:-} PRINTABLES_TOKEN: ${PRINTABLES_TOKEN:-} volumes: - # Folders on the host, under ./data (set DATA_DIR to put them elsewhere). + # 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 diff --git a/tools/volumes-to-folders.sh b/tools/volumes-to-folders.sh index cbef307..ab9b55f 100755 --- a/tools/volumes-to-folders.sh +++ b/tools/volumes-to-folders.sh @@ -8,9 +8,23 @@ # 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)")}" @@ -20,13 +34,14 @@ if docker compose ps --status running --quiet 2>/dev/null | grep -q .; then exit 1 fi -echo "Project: $PROJECT" -echo "Copying into: $DATA_DIR" +echo "Project: $PROJECT" +echo "Files: $DATA_DIR" +echo "Database: $PGDATA_DIR" echo for name in "${VOLUMES[@]}"; do volume="${PROJECT}_${name}" - target="$DATA_DIR/$name" + target="$(target_for "$name")" if ! docker volume inspect "$volume" >/dev/null 2>&1; then echo "· $volume — no such volume, skipping (nothing to copy)"