How to Configure Media Storage with Filesystem in Listmonk: A Complete Guide
To store uploaded campaign media on your local server, set upload.provider = "filesystem" in config.toml and define upload.path, upload.uri, and upload.root_url to specify the storage directory and public URL scheme.
Listmonk, the high-performance open-source newsletter manager maintained in the knadh/listmonk repository, delegates media storage to a pluggable provider interface. While Amazon S3 is supported for object storage, production deployments often require simple local filesystem storage for images and attachments. Configuring the filesystem provider allows you to persist media directly on the server's disk without external dependencies.
Filesystem Provider Architecture
Listmonk abstracts storage behind the media.Store interface defined in internal/media/media.go. When you select the filesystem provider, Listmonk instantiates the local store implemented in internal/media/providers/filesystem/filesystem.go instead of the S3 backend.
Core Implementation in filesystem.go
The provider relies on an Opts struct that maps directly to your configuration file:
type Opts struct {
UploadPath string `koanf:"upload_path"` // Absolute or relative directory path
UploadURI string `koanf:"upload_uri"` // URL path prefix for public access
RootURL string `koanf:"root_url"` // Base URL of the Listmonk instance
}
Source: internal/media/providers/filesystem/filesystem.go (lines 12‑16).
When a file uploads via the API, the Put() method writes bytes to UploadPath/filename, while GetURL() concatenates RootURL + UploadURI + filename to generate the public access URL served to subscribers.
Configuration Settings for Filesystem Storage
All provider-specific settings live under the [upload] section of your TOML configuration. The mapping between configuration keys and code occurs in cmd/init.go (around lines 506‑507), where koanf populates the Config.MediaUpload struct.
Required TOML Configuration
Add the following block to config.toml:
[upload]
provider = "filesystem" # Activates the local filesystem driver
path = "./uploads" # Server directory where files are written
uri = "/uploads" # Public URL path prefix (concatenated to root_url)
root_url = "https://newsletter.example.com"
upload.provider: Must be exactly"filesystem"to trigger the local storage initialization.upload.path: Corresponds toOpts.UploadPath. If omitted, it defaults to the current working directory (os.Getwd()).upload.uri: Corresponds toOpts.UploadURI. Optional; defaults to an empty string.upload.root_url: Corresponds toOpts.RootURL. Optional; defaults to an empty string (generating relative URLs).
Configuring Access Permissions
The Listmonk process requires read/write access to the target directory:
mkdir -p /var/lib/listmonk/media
chmod 755 /var/lib/listmonk/media
chown listmonk:listmonk /var/lib/listmonk/media
Step-by-Step Setup Guide
- Create the storage directory on your server with appropriate permissions.
- Edit
config.tomland populate the[upload]section with the values shown above. - Restart Listmonk to reload configuration. The application initializes the filesystem provider during startup by reading
Config.MediaUpload.Providerand constructing the store with yourOpts. - Verify functionality by uploading a test image through the admin UI or API; the file should appear in your configured
pathdirectory.
API Interaction and File Handling
Uploading Media
When you POST to /api/media, the handler in cmd/media.go receives the multipart form and calls the provider's Put() method:
curl -X POST https://newsletter.example.com/api/media \
-H "Authorization: Bearer $API_TOKEN" \
-F "file=@/path/to/campaign-header.png"
The InsertMedia function in internal/core/media.go records the metadata, while the filesystem provider atomically writes the file to UploadPath/filename.
Retrieving File URLs
The JSON response contains a public URL constructed by the provider:
{
"id": 42,
"filename": "campaign-header.png",
"url": "https://newsletter.example.com/uploads/campaign-header.png",
"content_type": "image/png"
}
This URL follows the pattern RootURL + UploadURI + filename, as implemented in the GetURL() method of filesystem.go.
Serving and Deleting Files
- Retrieval: When a subscriber accesses the public URL, Listmonk translates the request to a filesystem lookup. Internally,
GetBlob()reads the file fromUploadPath/filenameand streams it to the client. - Deletion: Calling
DELETE /api/media/{id}triggers theDelete()method, which removes the physical file fromUploadPathand purges the database row viacore.DeleteMedia.
Default Behavior and Fallbacks
If you omit configuration values, the provider applies safe defaults:
- Missing
upload.path: Falls back to the process working directory (wherelistmonkbinary executes). - Missing
upload.uri: Results in URLs that omit a path prefix (e.g.,https://example.com/filename). - Missing
upload.root_url: Produces relative URLs (/uploads/filename), which assumes the media is served from the same domain as the API.
These defaults are hardcoded in the Opts struct initialization logic within the filesystem provider constructor.
Summary
- Configure filesystem storage by setting
upload.provider = "filesystem"inconfig.toml. - Define
upload.pathfor physical storage,upload.urifor the public path prefix, andupload.root_urlfor the base domain. - The provider implementation lives in
internal/media/providers/filesystem/filesystem.go, exposing standardPut,GetURL,GetBlob, andDeletemethods. - Uploaded files are written to
UploadPath/filenameand served via URLs constructed asRootURL+UploadURI+filename. - Configuration mapping occurs in
cmd/init.go, while API handlers reside incmd/media.goand database operations ininternal/core/media.go.
Frequently Asked Questions
What happens if I do not specify an upload path?
If upload.path is omitted, the filesystem provider defaults to the current working directory of the Listmonk process, obtained via os.Getwd(). While functional, this is unpredictable in containerized environments, so explicit paths are recommended.
How does Listmonk generate public URLs for locally stored files?
The provider's GetURL() method concatenates three values from your configuration: root_url (scheme and host), uri (path prefix), and the stored filename. For example, with root_url = "https://example.com" and uri = "/media", a file named logo.png receives the URL https://example.com/media/logo.png.
Can I switch from filesystem to S3 without losing existing media?
Migrating requires copying physical files from your local upload.path to the S3 bucket, then updating upload.provider to "s3" and adjusting the S3-specific keys. Existing database records reference filenames only; if the filenames remain identical and the new upload.root_url points to the S3 endpoint, URLs will resolve correctly after the migration.
What file permissions does the upload directory require?
The user running the Listmonk process (often listmonk or www-data) needs read and write permissions on the directory. Execute permission (+x) is also required to traverse into subdirectories if you configure nested paths like ./media/campaigns. Avoid 777 permissions; instead, use 755 with proper ownership.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →