Start with Self-Host Stuff Stash. Use this page when you need to change or maintain the installation.
Trust The Local Certificate
Section titled “Trust The Local Certificate”Caddy creates one local certificate authority for the web app, API, Dex, and Garage. Export its root certificate:
mkdir -p .stuffstash/selfhost/caddydocker compose -f compose.selfhost.yaml cp caddy:/data/caddy/pki/authorities/local/root.crt .stuffstash/selfhost/caddy/root.crtCopy root.crt to each device that opens Stuff Stash, then import it using the
matching instructions below.
macOS
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain .stuffstash/selfhost/caddy/root.crtUbuntu or Debian
sudo cp .stuffstash/selfhost/caddy/root.crt /usr/local/share/ca-certificates/stuffstash.crtsudo update-ca-certificatesFedora or RHEL
sudo cp .stuffstash/selfhost/caddy/root.crt /etc/pki/ca-trust/source/anchors/stuffstash.crtsudo update-ca-trustWindows
Run in an administrator PowerShell window:
Import-Certificate -FilePath .\root.crt -CertStoreLocation Cert:\LocalMachine\RootFirefox may use its own certificate store. If the warning remains, open Settings → Privacy & Security → Certificates → View Certificates → Authorities, then import the root.
Replace The Example Credentials
Section titled “Replace The Example Credentials”The defaults are reasonable for a trusted home network, but they are public. Replace them before exposing Stuff Stash to a wider network.
Do this before adding data because the clean reset removes the example stack:
- Stop and reset with
docker compose -f compose.selfhost.yaml down -v. - Replace the Dex users and clients.
- Replace every
change-me-value in.envand generate a provider key withopenssl rand -base64 32. - Run
./scripts/selfhost-preflight.sh --strict. - Start with
docker compose -f compose.selfhost.yaml up -d, then trust its new Caddy root.
Keep .env and the private Dex config out of Git. Do not change database or
Garage credentials on an installation with data unless you also rotate them in
those services.
Keep The Address Stable
Section titled “Keep The Address Stable”Reserve the server’s IPv4 address in your router so bookmarks, certificates,
and OIDC callbacks do not change. If the address changes before you have data,
stop the stack, remove .env and .stuffstash, and run the setup again with
the new address.
Optional DNS Name
Section titled “Optional DNS Name”A local DNS name is optional. Stop the stack, replace the LAN IP everywhere in
.env and the private Dex config, run preflight, then start it again. The name
must resolve to the server on every client device; trust the new Caddy root if
the certificate authority changed.
What To Back Up
Section titled “What To Back Up”Back up .env, your private Dex config, and these Docker volumes together:
| Volume | Contents |
|---|---|
stuffstash_selfhost-postgres-data |
Inventory metadata and audit history |
stuffstash_selfhost-spicedb-postgres-data |
Authorization relationships |
stuffstash_selfhost-garage-meta |
Garage object metadata |
stuffstash_selfhost-garage-data |
Uploaded files |
stuffstash_selfhost-caddy-data |
Local CA and certificates |
Stop the stack before copying the volumes. Start it afterward, and test a restore before relying on the backup.
Upgrade
Section titled “Upgrade”- Back up the files and volumes above.
- Download and verify the new bundle in an empty directory.
- Copy the new
.env.exampleto.env, then carry over your changed values; do not replace the new file wholesale. Move the private Dex config too. - Stop the old bundle with
docker compose -f compose.selfhost.yaml down. - Run
./scripts/selfhost-preflight.sh, then start the new bundle.
The fixed Compose project name reuses the existing volumes. Check the app and an uploaded image after every upgrade. Database migrations may require the pre-upgrade backup to roll back.
Remove Everything
Section titled “Remove Everything”Send API Telemetry To A Collector
Section titled “Send API Telemetry To A Collector”The API can send traces, metrics, and structured events over OTLP/HTTP, and push Go runtime profiles to a Pyroscope-compatible server. Both are off by default. Store authentication values in your deployment’s secret manager.
For OTLP, configure:
| Variable | Value |
|---|---|
STUFF_STASH_TELEMETRY_ENABLED |
true |
OTEL_SERVICE_NAME |
stuffstash-api |
OTEL_SERVICE_VERSION |
Your deployed release version |
STUFF_STASH_DEPLOYMENT_ENVIRONMENT |
Your environment name |
OTEL_EXPORTER_OTLP_ENDPOINT |
Collector base URL, before /v1/traces or /v1/metrics |
OTEL_EXPORTER_OTLP_HEADERS |
Collector authentication, such as Authorization=Basic%20… |
For Grafana Cloud, copy the OTLP endpoint and authentication header from the stack’s
OpenTelemetry connection instructions. Use an access-policy token with
traces:write, metrics:write, and logs:write. A Grafana service-account token
used to manage dashboards is a different credential.
To enable profiling, also configure:
| Variable | Value |
|---|---|
STUFF_STASH_PROFILING_ENABLED |
true |
STUFF_STASH_PROFILING_ENDPOINT |
Profiles server URL from your collector |
STUFF_STASH_PROFILING_USERNAME |
Grafana Cloud Profiles instance ID |
STUFF_STASH_PROFILING_PASSWORD |
Access-policy token with profiles:write |
Profiling uses the same service/version/environment identity. It pushes CPU,
allocation, heap, goroutine, mutex, and blocking profiles; it does not open a
public profiling endpoint. The default upload interval is 15 seconds. See
Configuration and .env.example for optional settings.
After deploying, exercise an authenticated API request and confirm all four
signals arrive under the expected service and release. Check local logs for
telemetry.batches_dropped or profiling.delivery_failed. Successful startup or
shutdown alone does not prove delivery. Measure workload latency and resource use
with collection enabled before choosing sampling settings.
The adapters are tested against local collectors in CI, including authentication and redirect handling. An isolated remote probe has also verified stored metrics, logs, traces, and runtime profiles in Grafana Cloud. This does not establish deployed API or physical-device performance.
A reusable dashboard is available at
deploy/observability/image-performance-dashboard.json. Import it in Grafana,
select the Prometheus data source, then select the API service. It shows request
and dependency latency, cache response rates, runtime resources, and failed
export batches. It assumes the standard OTLP-to-Prometheus metric-name mapping;
runtime and duration metric mappings have been verified against Grafana Cloud.
The thumbnail counter mapping awaits an authenticated image workload. Use Tempo, Loki,
and Profiles to inspect the corresponding service and time window.
Background Thumbnails
Section titled “Background Thumbnails”The API prepares small, medium, and large thumbnails after an image upload or import. Uploads finish before thumbnail processing. Until a thumbnail is ready, opening it can still require processing; subsequent reads use the stored result. Original images and authenticated image URLs stay the same.
Workers run inside the API and keep their queue in the database. Restarting the API does not lose pending work. Start with the default concurrency of one; increase it to two only after checking memory use and API response times with your own photos. Foreground generation and background workers share this limit within each API process. Large camera photos can use much more memory than their compressed files.
Set these environment variables on the API (in .env for the self-host Compose setup):
| Variable | Default | Purpose |
|---|---|---|
STUFF_STASH_THUMBNAIL_CONCURRENCY |
1 |
Concurrent image jobs per API process; accepts 1–8. |
STUFF_STASH_THUMBNAIL_WORKER_ENABLED |
true |
Process queued images in the background. |
STUFF_STASH_THUMBNAIL_BACKFILL_ENABLED |
false |
Queue images uploaded before this feature. |
STUFF_STASH_THUMBNAIL_BACKFILL_BATCH_SIZE |
25 |
Attachment records scanned per batch. |
STUFF_STASH_THUMBNAIL_BACKFILL_INTERVAL |
5s |
Pause between scan batches. |
STUFF_STASH_THUMBNAIL_MAX_ATTEMPTS |
5 |
Attempts before a job is marked failed. |
For the initial upgrade to background thumbnails, stop every old API instance before running migrations and starting the new API, keeping backfill disabled. This requires a brief interruption so older processes cannot write during migration. Then enable backfill and restart the API through your normal deployment process. The scan saves its progress and stops when complete. Newly uploaded images are processed ahead of backfill jobs. Disabling the worker pauses queued processing; it does not disable foreground generation or periodic blob cleanup.
Inspect the queue from the running container:
docker compose -f compose.selfhost.yaml exec app /app/stuff-stash thumbnail-jobs statusThe JSON result reports pending, leased, failed and completed counts, the oldest pending age in seconds, and whether backfill has completed. It contains no image identifiers. After resolving a storage or processing problem, retry a bounded batch:
docker compose -f compose.selfhost.yaml exec app /app/stuff-stash thumbnail-jobs retry-failed --limit 100Retry accepts 1–1000 failed jobs and resets their attempt count. It leaves active and completed jobs alone. These commands use the container’s database credentials and write operational logs to stderr. The command entrypoint is covered by CI; the Compose examples have not yet been exercised against a released image.
Set up labels and printing
Section titled “Set up labels and printing”After setup, open an item’s, container’s, or location’s More → Label options to preview and save a PNG or PDF. The initial size is 29 × 90 mm; choose QR with title or QR only independently. Print the PDF at actual size / 100% using your system print controls. Saving a file or opening printing does not confirm that a physical label printed. For direct printing, register the Brother QL-800 USB printer on its Linux computer with 29 × 90 mm labels. See printer setup.
Use Browse → Scan label to aim the camera at a label. Choose Enter label link to paste instead, or use the flashlight in dim light. If camera access is unavailable, the link field appears automatically. External labels open directly from Home or Browse; while you are in another task, an offer lets you open the label without interrupting your work. Labels require sign-in and current inventory access; they do not make items public. An ordinary camera opens the web sign-in page, which also offers Open in Stuff Stash for self-hosted domains without app links.
The self-host Compose configuration uses STUFF_STASH_WEB_ORIGIN for label
links and connector approval pages. To use another HTTPS web address, set
STUFF_STASH_PUBLIC_WEB_BASE_URL in .env. You can separately set
STUFF_STASH_LABEL_BASE_URL to a stable HTTPS address, including any path prefix;
it must route to the web app’s label landing page. Recreate the API container
after changing these settings.
After applying database migrations, initialize the instance identity once:
docker compose -f compose.selfhost.yaml exec app /app/stuff-stash labels bootstrap-instanceThe command uses the container’s database settings. Repeating it returns the same identity. Keep that identity and the label mappings in full database backups when moving the instance. JSON/CSV imports into another instance do not preserve label links. An ordinary camera still needs the printed hostname to remain reachable after an address change.
To make an independent copy, start with a fresh database, run the bootstrap command above, and restore an inventory archive. A portable archive restore creates new inventory and asset IDs; the spreadsheet JSON/CSV exports are not a restore format. The copy gets its own instance identity and label mappings; print new labels for it. Do not run a full database restore as a separate instance alongside the original: that preserves the original label identity. The bootstrap command does not reset an existing identity.
Downloads require current inventory access and expire after one hour by default. See label configuration. The bootstrap command has been verified with persistent SQLite; this Compose example has not yet been exercised against a released image.
