Skip to content

Self-Host Operations

Manage certificates, users, backups, and upgrades for Stuff Stash.

Start with Self-Host Stuff Stash. Use this page when you need to change or maintain the installation.

Caddy creates one local certificate authority for the web app, API, Dex, and Garage. Export its root certificate:

Terminal window
mkdir -p .stuffstash/selfhost/caddy
docker compose -f compose.selfhost.yaml cp caddy:/data/caddy/pki/authorities/local/root.crt .stuffstash/selfhost/caddy/root.crt

Copy root.crt to each device that opens Stuff Stash, then import it using the matching instructions below.

macOS
Terminal window
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain .stuffstash/selfhost/caddy/root.crt
Ubuntu or Debian
Terminal window
sudo cp .stuffstash/selfhost/caddy/root.crt /usr/local/share/ca-certificates/stuffstash.crt
sudo update-ca-certificates
Fedora or RHEL
Terminal window
sudo cp .stuffstash/selfhost/caddy/root.crt /etc/pki/ca-trust/source/anchors/stuffstash.crt
sudo update-ca-trust
Windows

Run in an administrator PowerShell window:

Terminal window
Import-Certificate -FilePath .\root.crt -CertStoreLocation Cert:\LocalMachine\Root

Firefox may use its own certificate store. If the warning remains, open Settings → Privacy & Security → Certificates → View Certificates → Authorities, then import the root.

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:

  1. Stop and reset with docker compose -f compose.selfhost.yaml down -v.
  2. Replace the Dex users and clients.
  3. Replace every change-me- value in .env and generate a provider key with openssl rand -base64 32.
  4. Run ./scripts/selfhost-preflight.sh --strict.
  5. 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.

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.

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.

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.

  1. Back up the files and volumes above.
  2. Download and verify the new bundle in an empty directory.
  3. Copy the new .env.example to .env, then carry over your changed values; do not replace the new file wholesale. Move the private Dex config too.
  4. Stop the old bundle with docker compose -f compose.selfhost.yaml down.
  5. 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.

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.

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:

Terminal window
docker compose -f compose.selfhost.yaml exec app /app/stuff-stash thumbnail-jobs status

The 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:

Terminal window
docker compose -f compose.selfhost.yaml exec app /app/stuff-stash thumbnail-jobs retry-failed --limit 100

Retry 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.

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:

Terminal window
docker compose -f compose.selfhost.yaml exec app /app/stuff-stash labels bootstrap-instance

The 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.