Skip to content

FUTO Notes server configuration (environment variables)

The FUTO Notes sync server is configured entirely through environment variables. It has no config file of its own and no settings UI. Most installs need only one variable, the sync password; everything else has a working default.

Where you set a variable depends on how you run the server, so start there.

Where settings go

Docker Compose (what the installer sets up). Put settings in the .env file next to docker-compose.yml. Docker Compose reads that file, not the server: Compose substitutes its values into the compose file, and only the variables the compose file names reach the container. The installer writes four:

FUTO_NOTES_PASSWORD="your sync password"
FUTO_NOTES_DATA_DIR=/home/you/futo-notes/futo-notes-data
FUTO_NOTES_PORT=3005
FUTO_NOTES_IMAGE=futotech/notes-server:stable

After you edit .env, run docker compose up -d from the install directory. Compose recreates the container with the new values. A plain restart does not pick up changes.

Release binary or systemd. The server reads real environment variables, plus a .env file in its working directory if one exists. With the systemd unit from the server README, put them in /etc/futo-notes-server.env and restart the service.

Compose variables

These go in the Compose .env. They are names the compose file understands, not server variables.

VariableDefaultWhat it does
FUTO_NOTES_PASSWORDnoneThe sync password. Passed to the server unchanged.
FUTO_NOTES_PASSWORD_HASHnoneA scrypt hash of the sync password, used instead of FUTO_NOTES_PASSWORD. See store a hash.
FUTO_NOTES_DATA_DIR./futo-notes-dataHost directory mounted at /data in the container. It holds db/notes.db and blobs/. Use an absolute path.
FUTO_NOTES_PORT3005Host port published for the container’s port 3000.
FUTO_NOTES_IMAGEfutotech/notes-server:stableServer image. Pin a tag here for predictable upgrades.
COOKIE_SECUREtruePassed to the server. See the table below.
BLOB_GC_ENABLEDtruePassed to the server. See the table below.

The compose file fixes the rest: AUTH_MODE=password, PORT=3000, BLOB_DIR=/data/blobs, and the image’s DATABASE_URL=sqlite:/data/db/notes.db.

FUTO_NOTES_PORT accepts an address as well as a port. FUTO_NOTES_PORT=127.0.0.1:3005 publishes the server on the loopback interface only, which is what you want when a reverse proxy on the same machine handles outside traffic.

Variables the compose file does not pass through

ALLOW_FRESH_DATABASE, DEV_UI, and the Postgres pool settings are not in the compose file, so putting them in .env does nothing. To set one, create docker-compose.override.yml beside docker-compose.yml. Compose merges it automatically:

services:
server:
environment:
ALLOW_FRESH_DATABASE: "true"

If you installed by hand and run Compose with -f docker-compose.production.yml, the override is not loaded automatically. Add -f docker-compose.override.yml after it.

Server environment variables

This is everything the server itself reads.

VariableDefaultWhat it does
FUTO_NOTES_PASSWORDnoneSync password in plain text. Required unless FUTO_NOTES_PASSWORD_HASH is set.
FUTO_NOTES_PASSWORD_HASHnoneScrypt hash of the sync password. Setting both password variables stops the server at startup.
DATABASE_URLsqlite:./data/notes.dbsqlite:<path>, postgres://... or postgresql://.... The Docker image sets sqlite:/data/db/notes.db. A relative SQLite path resolves against the working directory.
PORT3005Port the server listens on, on all interfaces. The Docker image sets 3000.
BLOB_DIR./blobsDirectory for encrypted blobs. The Docker image sets /data/blobs.
AUTH_MODEpasswordpassword or dev. Anything else stops the server.
COOKIE_SECUREtrueMarks the session cookie Secure. Only the exact value false turns it off. The FUTO Notes apps sign in with a bearer token, so this affects browsers only.
BLOB_GC_ENABLEDtrueOnly the exact value false turns off blob garbage collection. See server storage.
ALLOW_FRESH_DATABASEfalseOnly the exact value true overrides the fresh-database safety guard. See backup and restore.
DEV_UIfalseOnly the exact value true serves a developer test page at /dev.
DB_POOL_MAX10Postgres only. Maximum open connections.
DB_POOL_IDLE_TIMEOUT_MS10000Postgres only. Idle connection lifetime in milliseconds.

Warning: AUTH_MODE=dev replaces the password with a passwordless login that accepts any email address. Anyone who can reach the server can sign in. It exists for development and tests only.

Warning: DEV_UI=true adds routes under /dev that require no sign-in: a page that shows the database path, buttons that run the cleanup jobs (including blob garbage collection) on demand, and an endpoint that deliberately crashes a request. Leave it off on any server other people can reach.

For TLS to a Postgres server, put sslmode and sslrootcert in DATABASE_URL. New installs use SQLite, and moving an older Postgres install to SQLite is optional.

Store a password hash instead of the password

FUTO_NOTES_PASSWORD_HASH lets the .env file hold a hash rather than the password itself. The format is:

scrypt:N=16384,r=8,p=1:<salt as hex>:<64-byte key as hex>

The server always verifies with N=16384, r=8, p=1 and a 64-byte key, whatever the prefix says, so keep the prefix exactly as shown. There is no generator built into the server. This Python 3 snippet produces the right format; it needs a Python built against OpenSSL, which is the norm on Linux (the Python that ships with macOS lacks hashlib.scrypt):

python3 -c 'import getpass, hashlib, os
pw = getpass.getpass("Sync password: ").encode()
salt = os.urandom(16)
key = hashlib.scrypt(pw, salt=salt, n=16384, r=8, p=1, dklen=64)
print(f"scrypt:N=16384,r=8,p=1:{salt.hex()}:{key.hex()}")'

Hash the same password your devices already use. Then, in .env, delete the FUTO_NOTES_PASSWORD line, add the hash, and run docker compose up -d:

FUTO_NOTES_PASSWORD_HASH=scrypt:N=16384,r=8,p=1:07dbbaa9...:1dcbf1b5...

A hash is safer to keep on disk than the password, but it is not a secret you can publish: anyone holding it can try passwords against it offline. Keep .env at mode 600 either way. To change the password itself, see change the sync password.

The .env file format

When the server reads .env itself (binary installs), it follows these rules:

  • One KEY=value per line. Blank lines and lines starting with # are skipped. A leading export is allowed.
  • A value wrapped in matching single or double quotes has the quotes removed. Nothing inside is unescaped, so "a\"b" is read as a\"b.
  • In an unquoted value, a # that follows a space or tab starts a comment.
  • Variables already set in the environment win over .env. A missing .env is not an error.

Docker Compose parses its .env with its own rules, which do process escapes and $ substitution. The installer writes the password in double quotes, escapes \ and " with a backslash, and doubles every $. A file written for one parser is not always read the same way by the other, so if your password contains ", \ or $, write it for the one that will read it.

Fixed limits

These are protocol limits, not settings. There is no variable for any of them.

LimitValue
Login attempts10 per 60 seconds per client address. Over the limit, 429 with Retry-After. Behind a proxy, all clients share one address; see the rate limit behind a proxy.
One blob upload100 MiB (104,857,600 bytes)
One batch upload request32 MiB
Batch download request200 keys, 64 KiB request body
Session lifetime7 days from sign-in. Activity does not extend it.
Uploaded blob not yet attached to a noteDeleted after 24 hours
Replaced or deleted note versionsKept 365 days
Retry records for app requests30 days (successful creates are kept for the life of the vault)
Cleanup jobsFirst run 1 minute after start, then sessions every hour and storage maintenance every 6 hours

Ignored variables from the old server

The current server ignores the variables below, which the earlier TypeScript server read, and logs one warning per variable at startup so you know:

level=WARN msg="MAX_BLOB_BYTES is ignored: the upload limit is fixed at 100 MiB"
VariableWhy it is ignored
AUTH_RATE_LIMIT, AUTH_RATE_LIMIT_WINDOW_MSLogin rate limiting is fixed at 10 attempts per 60 seconds.
BLOB_GC_INTERVAL_MSMaintenance runs on the fixed built-in schedule.
BLOB_RETENTION_DAYSRetained blob lifetime is fixed at 365 days.
DB_SSL, DB_SSL_INSECUREConfigure TLS with sslmode in DATABASE_URL.
DB_SSL_CAConfigure TLS with sslrootcert in DATABASE_URL.
LOG_LEVELThe server uses its built-in log level.
MAX_BATCH_BYTESThe batch limit is fixed at 32 MiB.
MAX_BLOB_BYTESThe upload limit is fixed at 100 MiB.
TRUST_PROXYForwarded client addresses are never trusted. Rate limiting uses the direct peer address.

The warnings are safe to act on at your own pace: remove the variable and the warning goes away. The full source for all of this is in internal/config/config.go in the server repository.

Next steps