Skip to content

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

WhatWhere (installer defaults)Why
Data directory~/futo-notes/futo-notes-data, or FUTO_NOTES_DATA_DIR in .envEverything the server has: db/notes.db (with notes.db-wal and notes.db-shm) and blobs/
.env~/futo-notes/.envThe 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-notes
grep FUTO_NOTES_DATA_DIR .env
docker compose stop
sudo cp -a /home/you/futo-notes/futo-notes-data /backups/futo-notes-data-$(date +%F)
docker compose start

Use 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/blobs

Copy 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-server
sudo cp -a /srv/futo-notes /backups/futo-notes-$(date +%F)
sudo systemctl start futo-notes-server

This 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

  1. Stop the server: docker compose stop.
  2. Move the current data directory aside, and copy the backup into its place, keeping db/ and blobs/ together.
  3. 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.
  4. Start it and check it: docker compose up -d, then curl 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

  1. Install Docker and Docker Compose v2 on the new machine. Skip the installer: it would create a new, empty server.
  2. Stop the old server with docker compose stop and leave it stopped. Two servers taking writes from different devices will drift apart.
  3. 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/.
  4. If the data directory’s path changed, update FUTO_NOTES_DATA_DIR in .env.
  5. Run docker compose up -d in the install directory on the new machine and check /health.
  6. 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_DIR or DATABASE_URL changed, so the server is looking in the wrong place.
  • A restore or move copied blobs/ but not db/.
  • A binary install started from a different working directory, so the default relative path ./data/notes.db pointed 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