Back up, restore, and move a FUTO Notes server
To back up a FUTO Notes server, stop it, copy the data directory, and start it again. The data directory holds both the SQLite database and the encrypted blobs, and the server keeps nothing anywhere else. Back up the .env file too, but store it apart from the data, because it holds your sync password.
What to back up
| What | Where (installer defaults) | Why |
|---|---|---|
| Data directory | ~/futo-notes/futo-notes-data, or FUTO_NOTES_DATA_DIR in .env | Everything the server has: db/notes.db (with notes.db-wal and notes.db-shm) and blobs/ |
.env | ~/futo-notes/.env | The sync password (or its hash), the data directory path, the port, and the image tag |
docker-compose.yml and any docker-compose.override.yml | ~/futo-notes/ | Only if you changed them. The stock compose file can be downloaded again. |
The container itself holds no state, so there is nothing to back up inside it.
Keep .env out of the data backup
Store .env apart from the data directory backup, because .env holds your sync password in plain text. Your notes are encrypted on your devices before upload, so a copy of the data directory is unreadable without that password. Put the two in the same backup and anyone who gets hold of it has both the ciphertext and the key to it.
Keep .env somewhere else: a password manager, a different backup destination, or nowhere at all if you know the password, since the file is four lines you can write again. If you store a scrypt hash instead of the password, .env becomes less sensitive, though a hash still lets someone guess passwords offline.
Back up a Docker install
From the install directory:
cd ~/futo-notesgrep FUTO_NOTES_DATA_DIR .envdocker compose stopsudo cp -a /home/you/futo-notes/futo-notes-data /backups/futo-notes-data-$(date +%F)docker compose startUse the path grep printed. cp -a keeps file ownership: the files belong to user ID 1000, which the container runs as. rsync -a or tar work too, as long as they keep owners. The server is down only for the copy.
Why you stop the server first
Stopping the server makes every file still. Copying the files while the server writes to them can give you a database that does not open, and a copy of notes.db on its own can be missing your recent changes.
SQLite runs in write-ahead-log mode. Recent changes live in notes.db-wal until SQLite folds them into notes.db, and on a small server that can be nearly everything: a freshly used test server had a 4 KB notes.db and 177 KB of data in the WAL. Copy the whole directory so notes.db, notes.db-wal and blobs/ travel together.
Back up without stopping
If the host has the sqlite3 command-line tool, it can take a consistent copy of a live database. It is not in the server image, so run it on the host:
sudo sqlite3 /home/you/futo-notes/futo-notes-data/db/notes.db ".backup '/backups/notes.db'"sudo cp -a /home/you/futo-notes/futo-notes-data/blobs /backups/blobsCopy the database first, then the blobs. Blob files uploaded between the two steps are harmless extras: on restore, the server’s maintenance adopts files it has no record of and deletes them after 24 hours. To restore this kind of backup, put notes.db in db/ and blobs/ beside it.
Back up a binary or systemd install
Stop the service, copy the SQLite file named in DATABASE_URL together with its -wal and -shm files, copy BLOB_DIR, and start the service again:
sudo systemctl stop futo-notes-serversudo cp -a /srv/futo-notes /backups/futo-notes-$(date +%F)sudo systemctl start futo-notes-serverThis assumes the database and blobs both sit under /srv/futo-notes, as in the README’s layout. Keep /etc/futo-notes-server.env out of that backup for the same reason as .env.
Restore from a backup
- Stop the server:
docker compose stop. - Move the current data directory aside, and copy the backup into its place, keeping
db/andblobs/together. - Make sure user ID 1000 owns the files. The container fixes ownership of its top-level folders on start, but not of the files inside them:
sudo chown -R 1000:1000 /home/you/futo-notes/futo-notes-data. - Start it and check it:
docker compose up -d, thencurl http://localhost:3005/health.
Restoring puts the server back to the moment the backup was taken. Anything synced after that is gone from the server.
Move the server to a new machine
- Install Docker and Docker Compose v2 on the new machine. Skip the installer: it would create a new, empty server.
- Stop the old server with
docker compose stopand leave it stopped. Two servers taking writes from different devices will drift apart. - Copy the install directory (
.env,docker-compose.yml, any override file) and the data directory across, keeping ownership. For example, as root on both machines:rsync -a --numeric-ids /home/you/futo-notes/ newhost:/home/you/futo-notes/. - If the data directory’s path changed, update
FUTO_NOTES_DATA_DIRin.env. - Run
docker compose up -din the install directory on the new machine and check/health. - If the server’s address changed, update the Server URL in the app on each device. The sync password stays the same.
The fresh-database safety guard
On a SQLite install, the server refuses to start when its database file is missing or empty but BLOB_DIR already holds blob files. It logs this and exits:
level=ERROR msg="opening database" err="refusing to create fresh SQLite database \"/data/db/notes.db\" because BLOB_DIR \"/data/blobs\" contains blob files; DATABASE_URL may have been lost or changed (set ALLOW_FRESH_DATABASE=true to override)"Under Docker the container then restarts over and over, and docker compose logs shows the same line each time.
The guard exists because the database is the part you cannot rebuild. It holds the list of notes, which blob is the current version of each, and the wrapped copy of your vault key. Blob files alone cannot be turned back into a vault. Without the guard, a server that lost track of its database would start empty, and your devices would sync against an empty vault.
The usual causes:
FUTO_NOTES_DATA_DIRorDATABASE_URLchanged, so the server is looking in the wrong place.- A restore or move copied
blobs/but notdb/. - A binary install started from a different working directory, so the default relative path
./data/notes.dbpointed somewhere new.
Fix the path or restore the db/ folder, then start the server again.
ALLOW_FRESH_DATABASE=true turns the guard off. Use it only when you really want a new, empty vault beside old blob files. Those old files are never cleaned up: they belong to a user the new database does not know, so maintenance skips them. Moving the old data directory aside and starting with an empty one is usually the better choice. In a Docker install, the variable has to go in docker-compose.override.yml, not .env. Remove it again once the server is up.
Next steps
- Storage growth: why the data directory grows, and when old versions are removed.
- The sync password: what happens if you lose the password
.envholds. - Errors, health, and logs: what to check if the server will not start after a restore.