Support for alternative storage in latest Joplin Server 2.6.10

If anyone's interested I've updated the documentation to explain how to store content outside the database. The goal is to reduce the database load, to make it easier to backup and restore the database, and to reduce storage costs (if you're on a managed database instance).

Currently you have the option to save to the local filesystem or to S3. Feel free to post here if you have any question.

Here's the full update:


By default, the item contents (notes, tags, etc.) are stored in the database and you don't need to do anything special to get that working.

However since that content can be quite large, you also have the option to store it outside the database by setting the STORAGE_DRIVER environment variable.

Setting up storage on a new installation

Again this is optional - by default items will simply be saved to the database. To save to the local filesystem instead, use:

STORAGE_DRIVER=Type=File; Path=/path/to/dir

Then all item data will be saved under this /path/to/dir directory.

Migrating storage for an existing installation

Migrating storage is a bit more complicated because the old content will have to be migrated to the new storage. This is done by providing a fallback driver, which tells the server where to look if a particular item is not yet available on the new storage.

To migrate from the database to the file system for example, you would set the environment variables like so:

STORAGE_DRIVER=Type=File; Path=/path/to/dir
STORAGE_DRIVER_FALLBACK=Type=Database; Mode=ReadAndWrite

From then on, all new and updated content will be added to the filesystem storage. When reading an item, if the server cannot find it in the filesystem, it will look for it in the database.

Fallback drivers have two write modes:

  • In ReadAndClear mode, it's going to clear the fallback driver content every time an item is moved to the main driver. It means that over time the old storage will be cleared and all content will be on the new storage.

  • In ReadAndWrite mode, it's going to write the content to the fallback driver too. This is purely for safey - it allows deploying the new storage (such as the filesystem or S3) but still keep the old storage up-to-date. So if something goes wrong it's possible to go back to the old storage until the new one is working.

It's recommended to start with ReadAndWrite mode.

This simple setup with main and fallback driver is sufficient to start using a new storage, however old content that never gets updated will stay on the database. To migrate this content too, you can use the storage import command. It takes a connection string and move all items from the old storage to the new one.

For example, to move all content from the database to the filesytem:

docker exec -it CONTAINER_ID node packages/server/dist/app.js storage import --connection 'Type=File; Path=/path/to/dir'

On the database, you can verify that all content has been migrated by running this query:

SELECT count(*), content_storage_id FROM items GROUP BY content_storage_id;

If everything went well, all items should have a content_storage_id > 1 ("1" being the database).

Other storage driver

Besides the database and filesystem, it's also possible to use AWS S3 for storage using the same environment variable:

STORAGE_DRIVER=Type=S3; Region=YOUR_REGION_CODE; AccessKeyId=YOUR_ACCESS_KEY; SecretAccessKeyId=YOUR_SECRET_ACCESS_KEY; Bucket=YOUR_BUCKET
10 Likes

I really love this feature - it will allow efficient backups of the joplin server without duplicated binary storage..

At the moment I hit following issues:

  1. the volume in the docker container needs correct right assignments (in my case it was 1001:1001 - I wonder if this is static)
  2. it looks files remain in storage after the note has been deleted (but not visible in joplin trash) - sync tell "1 remote item deleted", joplin server shows DELETE method on some note..

here is the log of the server (after initial sync, I added new note with attachment, synced, removed attachment, synced, removed the note)

2021-11-16 21:29:34: App: Content driver: { type: 2, path: '/mnt/files' }
2021-11-16 21:29:34: App: Content driver (fallback): { type: 1, mode: 1 }
2021-11-16 21:29:34: App: Trying to connect to database...
2021-11-16 21:29:34: App: Connection check: {
  latestMigration: { name: '20211111134329_storage_index.js', done: true },
  isCreated: true,
  error: null
}
2021-11-16 21:29:36: App: Auto-migrating database...
2021-11-16 21:29:36: App: Latest migration: { name: '20211111134329_storage_index.js', done: true }
2021-11-16 21:29:36: App: Starting services...
2021-11-16 21:29:36: ShareService: Starting maintenance...
2021-11-16 21:29:36: EmailService: Service will be disabled because mailer config is not set or is explicitly disabled
2021-11-16 21:29:36: TaskService: Scheduling #1 (Delete expired tokens): 0 */6 * * *
2021-11-16 21:29:36: TaskService: Scheduling #2 (Update total sizes): 0 * * * *
2021-11-16 21:29:36: TaskService: Scheduling #3 (Process oversized accounts): 0 */2 30 * *
2021-11-16 21:29:36: App: Performing main storage check...
2021-11-16 21:29:36: App: Item was written, read back and deleted without any error.
2021-11-16 21:29:36: App: Performing fallback storage check...
2021-11-16 21:29:36: App: Database storage is special and cannot be checked this way. If the connection to the database was successful then the storage driver should work too.
2021-11-16 21:29:36: App: Call this for testing: `curl https://joplin.mycloud.tld/api/ping`
2021-11-16 21:29:36: ShareService: Maintenance completed in 26ms
2021-11-16 21:31:13: App: POST /api/sessions (200) (126ms)
2021-11-16 21:31:14: App: GET /api/share_users (200) (6ms)
2021-11-16 21:31:14: App: GET /api/shares (200) (10ms)
2021-11-16 21:31:14: App: POST /api/sessions (200) (116ms)
2021-11-16 21:31:14: App: GET /api/items/root:/info.json:/content (200) (68ms)
2021-11-16 21:31:15: App: GET /api/items/root:/locks/*:/children (200) (6ms)
2021-11-16 21:31:15: App: GET /api/items/root:/locks/*:/children (200) (3ms)
2021-11-16 21:31:15: App: PUT /api/items/root:/temp/timeCheck911224.txt:/content (200) (40ms)
2021-11-16 21:31:15: App: GET /api/items/root:/temp/timeCheck911224.txt: (200) (2ms)
2021-11-16 21:31:15: App: PUT /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json:/content (200) (38ms)
2021-11-16 21:31:15: App: DELETE /api/items/root:/temp/timeCheck911224.txt: (200) (43ms)
2021-11-16 21:31:15: App: GET /api/items/root:/locks/*:/children (200) (4ms)
2021-11-16 21:31:15: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:31:15: App: GET /api/items/root:/:/delta (200) (6ms)
2021-11-16 21:31:15: App: DELETE /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json: (200) (21ms)
2021-11-16 21:31:15: App: GET /api/share_users (200) (3ms)
2021-11-16 21:31:15: App: GET /api/shares (200) (2ms)
2021-11-16 21:33:36: App: GET /api/items/root:/info.json:/content (200) (5ms)
2021-11-16 21:33:36: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:33:36: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:33:36: App: PUT /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json:/content (200) (51ms)
2021-11-16 21:33:36: App: GET /api/items/root:/locks/*:/children (200) (3ms)
2021-11-16 21:33:36: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:33:37: App: PUT /api/batch_items (200) (151ms)
2021-11-16 21:33:37: App: PUT /api/batch_items (200) (32ms)
2021-11-16 21:33:37: App: PUT /api/items/root:/.resource/55c749c819aa4368809aed37ed65163e:/content (200) (72ms)
2021-11-16 21:33:37: App: PUT /api/items/root:/55c749c819aa4368809aed37ed65163e.md:/content (200) (46ms)
2021-11-16 21:33:37: App: GET /api/items/root:/:/delta (200) (5ms)
2021-11-16 21:33:37: App: DELETE /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json: (200) (25ms)
2021-11-16 21:33:37: App: GET /api/share_users (200) (2ms)
2021-11-16 21:33:38: App: GET /api/shares (200) (4ms)
2021-11-16 21:33:47: ShareService: Starting maintenance...
2021-11-16 21:33:47: ShareService: Maintenance completed in 72ms
2021-11-16 21:35:36: App: GET /api/items/root:/info.json:/content (200) (6ms)
2021-11-16 21:35:36: App: GET /api/items/root:/locks/*:/children (200) (3ms)
2021-11-16 21:35:36: App: GET /api/items/root:/locks/*:/children (200) (3ms)
2021-11-16 21:35:36: App: PUT /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json:/content (200) (48ms)
2021-11-16 21:35:36: App: GET /api/items/root:/locks/*:/children (200) (3ms)
2021-11-16 21:35:36: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:35:36: App: GET /api/items/root:/34df002a4dcf45218b05b99edb27e8d7.md: (200) (1ms)
2021-11-16 21:35:37: App: GET /api/items/root:/34df002a4dcf45218b05b99edb27e8d7.md:/content (200) (7ms)
2021-11-16 21:35:37: App: PUT /api/items/root:/34df002a4dcf45218b05b99edb27e8d7.md:/content (200) (69ms)
2021-11-16 21:35:37: App: PUT /api/items/root:/.resource/1806024dd5a6456b95f5eec919a9a3f7:/content (200) (632ms)
2021-11-16 21:35:38: App: PUT /api/items/root:/1806024dd5a6456b95f5eec919a9a3f7.md:/content (200) (65ms)
2021-11-16 21:35:38: App: GET /api/items/root:/:/delta (200) (7ms)
2021-11-16 21:35:38: App: DELETE /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json: (200) (24ms)
2021-11-16 21:35:38: App: GET /api/share_users (200) (2ms)
2021-11-16 21:35:38: App: GET /api/shares (200) (4ms)
2021-11-16 21:35:47: ShareService: Starting maintenance...
2021-11-16 21:35:47: ShareService: Maintenance completed in 71ms
2021-11-16 21:36:53: App: GET /api/items/root:/info.json:/content (200) (4ms)
2021-11-16 21:36:53: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:36:53: App: GET /api/items/root:/locks/*:/children (200) (1ms)
2021-11-16 21:36:53: App: PUT /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json:/content (200) (33ms)
2021-11-16 21:36:53: App: GET /api/items/root:/locks/*:/children (200) (3ms)
2021-11-16 21:36:53: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:36:54: App: GET /api/items/root:/34df002a4dcf45218b05b99edb27e8d7.md: (200) (1ms)
2021-11-16 21:36:54: App: GET /api/items/root:/34df002a4dcf45218b05b99edb27e8d7.md:/content (200) (6ms)
2021-11-16 21:36:54: App: PUT /api/items/root:/34df002a4dcf45218b05b99edb27e8d7.md:/content (200) (39ms)
2021-11-16 21:36:54: App: GET /api/items/root:/:/delta (200) (7ms)
2021-11-16 21:36:54: App: DELETE /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json: (200) (90ms)
2021-11-16 21:36:54: App: GET /api/share_users (200) (2ms)
2021-11-16 21:36:54: App: GET /api/shares (200) (3ms)
2021-11-16 21:37:04: ShareService: Starting maintenance...
2021-11-16 21:37:04: ShareService: Maintenance completed in 57ms
2021-11-16 21:37:29: App: GET /api/items/root:/info.json:/content (200) (3ms)
2021-11-16 21:37:29: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:37:29: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:37:29: App: PUT /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json:/content (200) (78ms)
2021-11-16 21:37:29: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:37:29: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:37:29: App: DELETE /api/items/root:/34df002a4dcf45218b05b99edb27e8d7.md: (200) (43ms)
2021-11-16 21:37:29: App: GET /api/items/root:/:/delta (200) (4ms)
2021-11-16 21:37:29: [error] App: 404: GET /api/items/root:/34df002a4dcf45218b05b99edb27e8d7.md:/content: 192.168.11.203: Not found: root:/34df002a4dcf45218b05b99edb27e8d7.md:
2021-11-16 21:37:29: App: GET /api/items/root:/34df002a4dcf45218b05b99edb27e8d7.md:/content (404) (4ms)
2021-11-16 21:37:29: App: DELETE /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json: (200) (17ms)
2021-11-16 21:37:29: App: GET /api/share_users (200) (1ms)
2021-11-16 21:37:30: App: GET /api/shares (200) (2ms)
2021-11-16 21:37:39: ShareService: Starting maintenance...
2021-11-16 21:37:39: ShareService: Maintenance completed in 107ms
2021-11-16 21:38:16: App: GET /api/items/root:/info.json:/content (200) (3ms)
2021-11-16 21:38:16: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:38:16: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:38:16: App: PUT /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json:/content (200) (41ms)
2021-11-16 21:38:17: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:38:17: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:38:17: App: DELETE /api/items/root:/73cd09da5bcb40bfb526e71ffcd60790.md: (200) (24ms)
2021-11-16 21:38:17: App: GET /api/items/root:/:/delta (200) (6ms)
2021-11-16 21:38:17: [error] App: 404: GET /api/items/root:/73cd09da5bcb40bfb526e71ffcd60790.md:/content: 192.168.11.203: Not found: root:/73cd09da5bcb40bfb526e71ffcd60790.md:
2021-11-16 21:38:17: App: GET /api/items/root:/73cd09da5bcb40bfb526e71ffcd60790.md:/content (404) (2ms)
2021-11-16 21:38:17: App: DELETE /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json: (200) (19ms)
2021-11-16 21:38:17: App: GET /api/share_users (200) (2ms)
2021-11-16 21:38:17: App: GET /api/shares (200) (2ms)
2021-11-16 21:38:27: ShareService: Starting maintenance...
2021-11-16 21:38:27: ShareService: Maintenance completed in 60ms
2021-11-16 21:40:01: App: GET /api/items/root:/info.json:/content (200) (3ms)
2021-11-16 21:40:01: App: GET /api/items/root:/locks/*:/children (200) (3ms)
2021-11-16 21:40:01: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:40:01: App: PUT /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json:/content (200) (51ms)
2021-11-16 21:40:01: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:40:01: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:40:01: App: GET /api/items/root:/:/delta (200) (3ms)
2021-11-16 21:40:01: App: DELETE /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json: (200) (30ms)
2021-11-16 21:40:01: App: GET /api/share_users (200) (1ms)
2021-11-16 21:40:01: App: GET /api/shares (200) (2ms)

at the moment it looks there is absolutely no link between note name/id/attachment id and what is visible in server log:

creating/syncing this note (i definitely saved the note at 22:55!):

results in this log output:

2021-11-16 21:51:52: App: DELETE /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json: (200) (25ms)
2021-11-16 21:51:52: App: GET /api/share_users (200) (1ms)
2021-11-16 21:51:52: App: GET /api/shares (200) (4ms)
2021-11-16 21:54:56: App: GET /api/items/root:/info.json:/content (200) (4ms)
2021-11-16 21:54:56: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:54:56: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:54:56: App: PUT /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json:/content (200) (47ms)
2021-11-16 21:54:57: App: GET /api/items/root:/locks/*:/children (200) (4ms)
2021-11-16 21:54:57: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:54:57: App: PUT /api/batch_items (200) (61ms)
2021-11-16 21:54:57: App: PUT /api/items/root:/.resource/8edb1f87fde6416091fd7ccd48f52740:/content (200) (188ms)
2021-11-16 21:54:57: App: PUT /api/items/root:/8edb1f87fde6416091fd7ccd48f52740.md:/content (200) (37ms)
2021-11-16 21:54:57: App: GET /api/items/root:/:/delta (200) (4ms)
2021-11-16 21:54:58: App: DELETE /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json: (200) (23ms)
2021-11-16 21:54:58: App: GET /api/share_users (200) (3ms)
2021-11-16 21:54:58: App: GET /api/shares (200) (4ms)
2021-11-16 21:55:07: ShareService: Starting maintenance...
2021-11-16 21:55:07: ShareService: Maintenance completed in 43ms
2021-11-16 21:55:54: App: GET /api/items/root:/info.json:/content (200) (4ms)
2021-11-16 21:55:54: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:55:54: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:55:54: App: PUT /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json:/content (200) (45ms)
2021-11-16 21:55:55: App: GET /api/items/root:/locks/*:/children (200) (3ms)
2021-11-16 21:55:55: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:55:55: App: GET /api/items/root:/:/delta (200) (3ms)
2021-11-16 21:55:55: App: DELETE /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json: (200) (29ms)
2021-11-16 21:55:55: App: GET /api/share_users (200) (1ms)
2021-11-16 21:55:55: App: GET /api/shares (200) (10ms)
2021-11-16 21:56:21: App: GET /api/items/root:/info.json:/content (200) (3ms)
2021-11-16 21:56:21: App: GET /api/items/root:/locks/*:/children (200) (1ms)
2021-11-16 21:56:21: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:56:21: App: PUT /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json:/content (200) (39ms)
2021-11-16 21:56:21: App: GET /api/items/root:/locks/*:/children (200) (3ms)
2021-11-16 21:56:21: App: GET /api/items/root:/locks/*:/children (200) (2ms)
2021-11-16 21:56:21: App: GET /api/items/root:/65163a7e90a74fbdb5b2b0361b958dcc.md: (200) (1ms)
2021-11-16 21:56:21: App: GET /api/items/root:/65163a7e90a74fbdb5b2b0361b958dcc.md:/content (200) (6ms)
2021-11-16 21:56:22: App: PUT /api/items/root:/65163a7e90a74fbdb5b2b0361b958dcc.md:/content (200) (37ms)
2021-11-16 21:56:22: App: GET /api/items/root:/:/delta (200) (7ms)
2021-11-16 21:56:22: App: DELETE /api/items/root:/locks/sync_desktop_a9c6344f64164838ad55bc076dd24f9c.json: (200) (33ms)
2021-11-16 21:56:22: App: GET /api/share_users (200) (1ms)
2021-11-16 21:56:22: App: GET /api/shares (200) (4ms)
2021-11-16 21:56:32: ShareService: Starting maintenance...
2021-11-16 21:56:32: ShareService: Maintenance completed in 69ms

@laurent

  • could you explain how to analyze the issue please? is there any way to know which attachment name belongs to which note ID?
  • could you add self-explaining logging like
    add/change/remove note {note guid} with name {name of the note}
    save/remove attachment id {attachment guid} for note {note guid} at /mnt/file/{filename}

UPDATE:

after the attachment has been deleted it is listed in "attachments" list

but there is no way to see it on the note history.. both note history and trash don't show anything - maybe this is the real problem?

Joplin 2.5.12 (prod, win32)

Client ID: a9c6344f64164838ad55bc076dd24f9c
Sync Version: 3
Profile Version: 39
Keychain Supported: Yes

Revision: 884b86f

The ID on Joplin are different but you can check the name on the items table to find out what they are.

Are you sure that the item is not deleted from the storage?

yes I'm sure the item remains there, I tested 3 times with different notes and attachments. The problem is I have existing collection with hundreds of notes and immediately after I added the files storage Joplin created dozens of folders and some small files.. it's not easy to spot the right file on the list when the only good criteria is the size..

the problem must be somewhere in the logic - after I delete the attachment from the note it is still shown in the attachment list - so the client doesn't think this must be deleted.. but the problem is why? if I create a note with attachment and remove this attachment - there should be either a history item or the attachment should disappear.. both is not the case: I have no history items, attachment is still listed in the list.. same if I delete the note - no items in trash and attachment is still in the list..

Resources aren't deleted immediately so it's unrelated. To check deletion on the server you should delete a note, sync, and verify that the note file is gone.

as you can see in the log I posted initially - there is DELETE method used for the note

so far I understand this is the same note I created before - but this doesn't result in removing attachments linked to this note..

Please tell me a good strategy to verify both client and server work as expected and what/how to log to isolate the root cause.

That's not a note but a lock. I'll check and see if there's an issue with deletion.

1 Like