App storage

By default an app's files reset on every restart. Turn on storage and one folder stops resetting.

When you need it

Most apps don't. Configuration comes from environment variables and data belongs in the managed database, which queries properly, works with several copies of your app at once, and changes nothing about how your deploys behave.

Storage is for the cases the database can't cover:

  • A SQLite file the app opens directly.
  • Files people upload, when you'd rather not put them in a database.
  • A tool that insists on its own data folder: n8n's, a self-hosted app's working directory, a search index it rebuilds slowly.
  • A cache that is expensive to rebuild and should survive a deploy.

Turning it on

Open the app, go to the Size tab, and pick a size underApp storage. Sizes are 10, 20, 30, 50 and 100 GB, taken from the same storage pool your managed database draws on. Storage can grow later; it can never shrink.

The free allowance covers a managed database or app storage, not both. If your app already has the managed database, adding storage needs more capacity on your plan. See pricing.

Your app reads DATA_DIR

Dockhold sets an environment variable called DATA_DIR pointing at the folder. Read it, don't hardcode a path. Everything you write inside that folder survives restarts and deploys; everything outside it is scratch space and resets, exactly as before.

import os
import sqlite3

# Injected by Dockhold. Everything under here survives a restart.
data_dir = os.environ["DATA_DIR"]
conn = sqlite3.connect(os.path.join(data_dir, "app.db"))
// Node
const path = require("node:path");
const dataDir = process.env.DATA_DIR;
const dbFile = path.join(dataDir, "app.db");

Write a fallback for local development (process.env.DATA_DIR ?? "./data") so the same code runs on your laptop.

Save your files on shutdown

When Dockhold stops your app it sends SIGTERM and waits up to 60 seconds before stopping it for good. Flush and close in that window: commit the open transaction, close the database handle, finish the write in progress. An app that ignores SIGTERM and buffers in memory can lose the last few seconds of work on every deploy.

process.on("SIGTERM", async () => {
  await db.close();       // finish writes, then exit
  server.close(() => process.exit(0));
});

What changes about deploys

Storage attaches to one running copy of the app at a time, so a deploy can no longer overlap the old version with the new one. Dockhold stops the running version, then starts the new one. Your app is unreachable for a few seconds in between.

That is the trade, and it is the same one everywhere else that offers a persistent disk. If an app can't take a short pause, keep its data in the managed database and leave storage off.

Two things storage rules out while it's on: running more than one copy of the app, and scheduled tasks. Both would need a second process writing to the same files, and a scheduled task runs separately, so it can't see the folder at all. Both are refused with the reason on screen, and both come back the moment you remove the storage.

Removing it

Remove storage from the same Size tab. It erases every file on it, there is no undo, and we keep no backup, so the dashboard asks you to confirm before it runs. Afterwards the app is back to scratch space only.

Keep your own copy

Storage is durable, not backed up. It survives restarts, deploys, and a machine failure underneath your app; it does not survive you deleting the app or removing the storage. There are no snapshots yet. If the files matter, copy them somewhere else on a schedule, or keep the data in the managed database.

One more case worth knowing: if a payment fails and the account drops back to the free plan, the files are kept for 30 days and then erased. The date is shown on the app's Size tab and in the banner on your dashboard, and everything comes back if you sort the billing out before then.

From your AI tool

The same thing over MCP, with a read+deploy token:set_app_storage(app_id, size_gb) adds or grows it,remove_app_storage(app_id, confirm) erases it. Callget_resource_usage first for the sizes your pool can take. SeeDeploy from your AI tool.

See also