Skip to main content

Configuration Overview

Configuration uses environment variables or command-line flags. Command-line flags take precedence over environment variables.

Security Reminder

By default, Mahresources runs with no authentication -- it is designed for private, trusted networks. Optional user accounts and role-based access control can be turned on with -auth; see Authentication & RBAC. Either way, do not expose it to the public internet without a reverse proxy that enforces authentication.

Configuration Methods

Environment Variables

Create a .env file in your working directory:

DB_TYPE=SQLITE
DB_DSN=./mahresources.db
FILE_SAVE_PATH=./files
BIND_ADDRESS=:8181

Command-Line Flags

Pass flags directly when starting the server:

./mahresources -db-type=SQLITE -db-dsn=./mahresources.db -file-save-path=./files -bind-address=:8181
Precedence

Command-line flags take precedence over environment variables, so a flag overrides the same setting from .env.

Quick Reference

FlagEnv VariableDescriptionDefault
-db-typeDB_TYPEDatabase type: SQLITE or POSTGRES-
-db-dsnDB_DSNDatabase connection string-
-db-readonly-dsnDB_READONLY_DSNRead-only database connection-
-db-log-fileDB_LOG_FILEDB log output: STDOUT, empty, or file path-
-db-slow-query-thresholdDB_SLOW_QUERY_THRESHOLDLog queries slower than this duration (e.g. 200ms) to the DB log and the application log0 (disabled)
-file-save-pathFILE_SAVE_PATHMain file storage directory-
-bind-addressBIND_ADDRESSServer address:port-
-memory-dbMEMORY_DB=1Use in-memory SQLite databasefalse
-memory-fsMEMORY_FS=1Use in-memory filesystemfalse
-ephemeralEPHEMERAL=1Fully ephemeral mode (memory DB + FS)false
-seed-dbSEED_DBSQLite file to seed memory-db-
-seed-fsSEED_FSDirectory for copy-on-write base-
-alt-fsFILE_ALT_*Alternative file systems-
-ffmpeg-pathFFMPEG_PATHPath to FFmpeg binaryauto-detect
-libreoffice-pathLIBREOFFICE_PATHPath to LibreOffice binaryauto-detect
-skip-ftsSKIP_FTS=1Skip Full-Text Search initializationfalse
-skip-version-migrationSKIP_VERSION_MIGRATION=1Skip resource version migrationfalse
-skip-block-ref-cleanupSKIP_BLOCK_REF_CLEANUP=1Skip one-shot cleanup of dangling note-block references at startupfalse
-max-db-connectionsMAX_DB_CONNECTIONSDatabase connection pool size0 (no limit)
-max-upload-sizeMAX_UPLOAD_SIZEMax resource/version upload body size in bytes; 0 = unlimited2147483648 (2 GiB)
-max-import-sizeMAX_IMPORT_SIZEMax group-import tar upload size in bytes10737418240 (10 GiB)
-max-json-bodyMAX_JSON_BODYMax application/json request body size in bytes; 0 disables the limit0 (unlimited)
-max-user-tokensMAX_USER_TOKENSMax API tokens a single user may hold; 0 disables the cap100
-max-job-concurrencyMAX_JOB_CONCURRENCYConcurrency budget for the shared background job manager6
-export-retentionEXPORT_RETENTIONHow long completed group-export tars stay on disk24h
-download-failed-retentionDOWNLOAD_FAILED_RETENTIONHow long a failed or cancelled download stays in the download history168h
-download-history-retentionDOWNLOAD_HISTORY_RETENTIONHow long a completed download stays in the download history (the resource it created is unaffected)24h
-download-cockpit-limitDOWNLOAD_COCKPIT_LIMITHow many finished downloads the jobs panel renders, newest first10
-plugin-schedule-tickPLUGIN_SCHEDULE_TICKHow often the plugin scheduler looks for due work; bounds the resolution of every plugin schedule30s
-max-action-entitiesMAX_ACTION_ENTITIESMaximum entities one plugin-action run may name; 0 selects the default rather than meaning unlimited1000
-hash-worker-countHASH_WORKER_COUNTConcurrent hash workers4
-hash-batch-sizeHASH_BATCH_SIZEResources per batch500
-hash-poll-intervalHASH_POLL_INTERVALTime between batch cycles1m
-hash-similarity-thresholdHASH_SIMILARITY_THRESHOLDMax Hamming distance for similarity10
-hash-worker-disabledHASH_WORKER_DISABLED=1Disable background hash workerfalse
-hash-cache-sizeHASH_CACHE_SIZEMax entries in hash similarity cache100000
-thumb-worker-countTHUMB_WORKER_COUNTConcurrent thumbnail workers2
-thumb-worker-disabledTHUMB_WORKER_DISABLED=1Disable thumbnail workerfalse
-thumb-batch-sizeTHUMB_BATCH_SIZEVideos per backfill cycle10
-thumb-poll-intervalTHUMB_POLL_INTERVALTime between backfill cycles1m
-thumb-backfillTHUMB_BACKFILL=1Backfill thumbnails for existing videosfalse
-video-thumb-timeoutVIDEO_THUMB_TIMEOUTTimeout for FFmpeg thumbnail generation30s
-video-thumb-lock-timeoutVIDEO_THUMB_LOCK_TIMEOUTTimeout waiting for thumbnail lock60s
-video-thumb-concurrencyVIDEO_THUMB_CONCURRENCYMax concurrent video thumbnail jobs4
-remote-connect-timeoutREMOTE_CONNECT_TIMEOUTTimeout for remote connections30s
-remote-idle-timeoutREMOTE_IDLE_TIMEOUTTimeout for idle transfers60s
-remote-overall-timeoutREMOTE_OVERALL_TIMEOUTMaximum total download time30m
-remote-user-agentREMOTE_USER_AGENTUser-Agent the server's own fetches sendbrowser-like default
-allow-private-fetchALLOW_PRIVATE_FETCHPrivate addresses/CIDR blocks the server's own fetches may reach (details)(none)
-mrql-query-timeoutMRQL_QUERY_TIMEOUTMaximum execution time for MRQL queries10s
-mrql-default-limitMRQL_DEFAULT_LIMITDefault LIMIT for MRQL queries without an explicit LIMIT500
-share-portSHARE_PORTPort for public share server(disabled)
-share-bind-addressSHARE_BIND_ADDRESSShare server bind address0.0.0.0
-share-public-urlSHARE_PUBLIC_URLExternally-routable base URL for shared notes(relative path)
-cleanup-logs-daysCLEANUP_LOGS_DAYSDelete log entries older than N days on startup0 (disabled)
-plugin-pathPLUGIN_PATHDirectory to scan for plugins./plugins
-plugins-disabledPLUGINS_DISABLED=1Disable the plugin system entirelyfalse
-authAUTH_ENABLED=1Enable user accounts + RBACfalse
-create-admin-userCREATE_ADMIN_USERBootstrap admin username at startup (idempotent; needs the password flag)-
-create-admin-passwordCREATE_ADMIN_PASSWORDPassword for -create-admin-user-
-session-cookie-secureSESSION_COOKIE_SECURE=1Mark the session cookie Secure (set behind TLS)false
Authentication

The rows above are the essentials for turning on auth. Session lifetime, login rate-limiting, and proxy-header trust have their own flags -- see Authentication & RBAC for the full reference.

Runtime vs. boot-only settings

Most flags apply only at startup. A curated subset can be changed at runtime via the admin UI, CLI, or API -- no restart needed.

Boot-only settings include: database DSN, bind addresses, file save path, ephemeral mode, alt filesystems, share port, FTS initialization, worker pool sizes, and max DB connections.

Fetching from your own network

Several features hand the server a URL and ask it to fetch: Add resource from URL (/v1/resource/remote), the background download queue, and the calendar block, which retrieves an .ics feed. In each case the URL comes from whoever is using the app, and the fetch happens from the server.

That means the server can be asked to fetch things the person asking could not reach themselves -- an admin panel on the internal network, a database's HTTP interface, or, on a cloud host, the instance metadata endpoint at 169.254.169.254, which hands out credentials to anything that asks. The response is then stored as a resource, or rendered on the page.

So by default the server refuses to fetch from any private address:

  • loopback (127.0.0.1, ::1)
  • link-local (169.254.0.0/16, including the metadata endpoint)
  • private ranges (10/8, 172.16/12, 192.168/16, fd00::/8)
  • carrier-grade NAT (100.64/10), multicast and broadcast
  • benchmarking (198.18.0.0/15), reserved (240.0.0.0/4), this-network (0.0.0.0/8), NAT64 (64:ff9b::/96, 64:ff9b:1::/48) and deprecated IPv6 site-local (fec0::/10), which Docker Desktop, VPN clients, some Kubernetes CNIs and NAT64 translators hand out

Public hosts are unaffected, with one exception. Downloading from the internet, the reason these features exist, works exactly as before with no configuration. The exception is 168.63.129.16: Azure numbered its platform-agent endpoint (WireServer) out of public address space, but it is host-internal on every Azure VM and serves goal-state and extension configuration to anything that asks, so it is refused like a private address. Azure's instance metadata service is a different address, 169.254.169.254, and is already covered as link-local. Name it in -allow-private-fetch if you genuinely need to reach it.

Allowing specific internal hosts

If you genuinely fetch from your own network -- a NAS, an internal calendar server, a file server -- name what it may reach:

./mahresources -allow-private-fetch=192.168.1.5,10.0.0.0/8
ALLOW_PRIVATE_FETCH=192.168.1.5,10.0.0.0/8

Two rules, both enforced at startup so a mistake is visible immediately rather than as a mysteriously failing download:

  • Name addresses or CIDR blocks, not hostnames. The check is applied to the address a name resolves to, so a hostname in this list could never match anything -- it would look like it permitted something while permitting nothing. Worse, a public hostname whose DNS record points at an internal address would otherwise sail through.

  • Blocks must be reasonably narrow. A prefix shorter than /8 (and the default route 0.0.0.0/0) is refused: it re-opens everything the setting exists to close, without saying so.

  • IPv6 blocks need a /32 or longer. The minimum prefix differs by family: /8 for IPv4, /32 for IPv6. Naming a whole IPv6 range such as fd00::/8 is refused for being too broad; name the specific block or address you need.

A refused fetch is reported to the user as a blocked request that does not name the address the URL resolved to -- otherwise a list of failed downloads would map your internal network for anyone allowed to submit one. The full detail, including the resolved address, is written to the activity log by all three paths, where an administrator can read it.

note

This does not apply to plugins, which have always declared their own network access in their manifest (network + allow_private_hosts). See Plugin permissions.

Common Configurations

Minimal Production Setup

./mahresources \
-db-type=SQLITE \
-db-dsn=./data/mahresources.db \
-file-save-path=./data/files \
-bind-address=:8181

Development/Testing (Ephemeral)

No persistence -- all data is lost when the server stops:

./mahresources -ephemeral -bind-address=:8181

Demo with Seeded Data

Load existing data for demos (changes stay in memory):

./mahresources \
-ephemeral \
-seed-db=./production.db \
-seed-fs=./production-files \
-bind-address=:8181

PostgreSQL with Read Replica

./mahresources \
-db-type=POSTGRES \
-db-dsn="host=primary.db user=app password=secret dbname=mahresources" \
-db-readonly-dsn="host=replica.db user=app password=secret dbname=mahresources" \
-file-save-path=/var/lib/mahresources/files \
-bind-address=:8181

Next Steps