Skip to main content
BOW keeps records in its database and file contents in file storage. Configure both before you use a deployment in production. The database alone is not enough: a file stays listed in BOW after its bytes are lost, because its database record still exists. BOW supports three file storage options, on Docker, Docker Compose and Kubernetes:

What BOW stores

All paths are inside the BOW container. Current versions store the organization logo under uploads/branding. The /app/backend/branding_uploads directory and the Compose branding_data volume are no longer used.

Object storage

With s3 or azure, the bucket or container holds every file BOW needs to keep. /app/backend/uploads becomes a local cache: BOW downloads a file the first time it needs it, so a new server, container or replica serves every file. For files that can be replaced, such as thumbnails and logos, BOW checks the bucket before serving a cached copy, so a replica never serves a file that another replica replaced or deleted.

Settings

Set storage in bow-config.yaml under storage:, or with BOW_STORAGE_* environment variables. A value in bow-config.yaml wins over the variable. The Helm chart sets the variables from its storage.* values.
Keep keys, secrets and SAS tokens out of bow-config.yaml: set them as environment variables from your secret store, or reference them with ${VAR} placeholders. With object storage, BOW_ARTIFACT_STORAGE is optional. Unset, data app files are cached in /app/backend/uploads/artifact_files.

Credentials

BOW needs read, write, delete and list on the bucket or container, under the prefix if you set one.
With SSE-KMS, also allow kms:GenerateDataKey and kms:Decrypt on the key. Keep the bucket private.
Assign Storage Blob Data Contributor on the container to the identity BOW uses. Keep anonymous access disabled. A SAS token needs container scope with read, add, create, write, delete and list.

Startup check

BOW checks the bucket when it starts. It writes, reads and deletes a small file under <prefix>_healthcheck/, logs the result and exits if any step fails:

Set it up

Pass the settings as environment variables, or mount a bow-config.yaml with a storage: block:
On EC2, auth: default uses the instance profile. Containers reach it only when the instance metadata hop limit is 2 or more (IMDS settings); otherwise set BOW_STORAGE_S3_AUTH=static with keys.

Move an existing installation to object storage

Plan a maintenance window and pause writes. Then copy the existing files into the bucket under the same names.
Keep the existing uploads volume mounted. Set the storage settings, update to a version with object storage, then copy the files from inside the container:
With plain Docker, use docker exec <container> instead of docker compose exec app. The script uploads every file BOW keeps, skips files that are already in the bucket, and never changes the local files. It also copies BOW_ARTIFACT_STORAGE when that is set. You can run it again safely. Once it finishes, the volume is only a cache.

Local disk

With storage.backend: local, files live in /app/backend/uploads inside the container. Mount a persistent volume there: a container’s own filesystem is lost when the container is recreated.
Mount a named volume:
For data app file resources on local disk, set BOW_ARTIFACT_STORAGE to a path on the same volume, for example /app/backend/uploads/artifact-files. Ensure the application user can create, read, and write files on the volume. BOW creates the folders it needs under /app/backend/uploads when it starts.

Migrate an existing Kubernetes installation

Use a maintenance window. Do not scale down or restart the old pods to begin this procedure: their local files may be the only copies.
  1. Pause changes without replacing pods. Suspend automated deployments and scaling, block new writes at your ingress or maintenance layer, and let active runs finish. Account for scheduled jobs and integrations that can still create files.
  2. Inventory every application pod. Record the image version, mounted paths, artifact storage path, Helm values, and encryption-key source. With several replicas, each pod may contain different files.
  3. Back up the database, original encryption key, and files. Copy the whole uploads directory from every pod into a separate backup directory, and the configured artifact storage path. For example, replace <pod-name> with one running application pod:
    Repeat with a different backup directory for each pod. Keep backups outside the pods and verify file counts, sizes, and checksums.
  4. Copy the files to the new storage. For object storage, upload them as in Move an existing installation to object storage. For a persistent volume, mount it in a temporary pod and restore the files, preserving directory structure and filenames. Combine copies from every source pod without silently overwriting conflicting files.
  5. Deploy the new storage settings. For a volume, stop the temporary pod first so single-node disks can detach. Keep the same database, encryption key, and configured file paths.
  6. Verify before resuming writes. Open existing uploads, generated files, the logo, and data app files. Then run the check below. Resume traffic, integrations, scheduling, and automated deployments after these checks pass.
Mounting an empty PVC can make existing files appear missing immediately because it hides the image or container directory beneath the mount. Restore your files before switching the application to the new volume.
Keep your existing config.secretRef pointing at a Secret containing the original BOW_ENCRYPTION_KEY. Do not generate a replacement key during a storage migration.

Verify storage after a restart

Test first in staging.
  1. Upload a small test file through BOW and record its contents. If you use data app file resources, test those too.
  2. With object storage, check that the startup log line File storage: ... names your bucket and prefix, and that the file appears under <prefix>files/. With local disk, check that your volume is mounted at /app/backend/uploads.
  3. Recreate the application: docker compose up -d --force-recreate app, or on Kubernetes:
  4. Open and download the same file through BOW and compare its contents. Also verify a pre-migration file and a stored connection credential.
  5. If you run multiple replicas, verify file access through each replica.
A successful health check or a Bound PVC alone does not verify that BOW is reading the persisted files.

Backups and recovery

Back up PostgreSQL, file storage, the original encryption key, and deployment configuration together with a consistent recovery point.
  • Object storage: turn on bucket versioning (S3) or blob soft delete and versioning (Azure), and use your provider’s backup or replication for the bucket or container.
  • Local disk: quiesce writes or use a backup procedure that coordinates database and volume snapshots. On Kubernetes, review your StorageClass reclaim policy before deleting claims or a release. A persistent volume is not a backup.
If files have already disappeared, restore the matching file backup and verify it against database records. A new volume cannot recover bytes from deleted containers. If there is no recoverable copy, re-upload the original files or regenerate the affected outputs. See Day 2 Operations for the wider backup and recovery checklist.