Compare commits

..

3 commits

Author SHA1 Message Date
dlawler489
01a6688900 Merge host folders for data
All checks were successful
build-and-push / docker (push) Successful in 17s
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-20 16:10:34 +10:00
dlawler489
6c10cf6b37 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 <noreply@anthropic.com>
2026-09-20 16:01:43 +10:00
dlawler489
985b81d1de 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 <noreply@anthropic.com>
2026-09-20 15:58:28 +10:00
4 changed files with 110 additions and 12 deletions

View file

@ -2,5 +2,13 @@
DATABASE_URL=postgres://USER@localhost:5432/stl_library DATABASE_URL=postgres://USER@localhost:5432/stl_library
PORT=3000 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 import — get a free app token at https://www.thingiverse.com/developers
THINGIVERSE_TOKEN= THINGIVERSE_TOKEN=

View file

@ -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`. 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 ## Configuration
Settings live in the database (editable in the app's **Settings** menu) and fall back to environment variables: Settings live in the database (editable in the app's **Settings** menu) and fall back to environment variables:

View file

@ -6,7 +6,11 @@ services:
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-printhive} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-printhive}
POSTGRES_DB: printhive POSTGRES_DB: printhive
volumes: 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: healthcheck:
test: ["CMD-SHELL", "pg_isready -U printhive -d printhive"] test: ["CMD-SHELL", "pg_isready -U printhive -d printhive"]
interval: 5s interval: 5s
@ -33,10 +37,12 @@ services:
MAKERWORLD_TOKEN: ${MAKERWORLD_TOKEN:-} MAKERWORLD_TOKEN: ${MAKERWORLD_TOKEN:-}
PRINTABLES_TOKEN: ${PRINTABLES_TOKEN:-} PRINTABLES_TOKEN: ${PRINTABLES_TOKEN:-}
volumes: volumes:
- uploads:/app/uploads # Folders on the host: ./data unless DATA_DIR says otherwise.
- thumbnails:/app/thumbnails # uploads and pgdata hold everything that matters; the other two rebuild.
- glb:/app/glb - ${DATA_DIR:-./data}/uploads:/app/uploads
- extracted:/app/extracted # cache of files unpacked from archives - ${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. # No host port published — Traefik auto-discovers the app on the shared network.
networks: networks:
- default # talk to the db - default # talk to the db
@ -52,13 +58,6 @@ services:
- "traefik.http.routers.printhive.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}" - "traefik.http.routers.printhive.tls.certresolver=${TRAEFIK_CERTRESOLVER:-le}"
- "traefik.http.services.printhive.loadbalancer.server.port=3000" - "traefik.http.services.printhive.loadbalancer.server.port=3000"
volumes:
pgdata:
uploads:
thumbnails:
glb:
extracted:
networks: networks:
default: default:
# The existing Traefik network (the `proxy` network Traefik runs on). # The existing Traefik network (the `proxy` network Traefik runs on).

68
tools/volumes-to-folders.sh Executable file
View file

@ -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 "<project>_<name>", 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