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 to Opts.UploadPath. If omitted, it defaults to the current working directory (os.Getwd()).
  • upload.uri: Corresponds to Opts.UploadURI. Optional; defaults to an empty string.
  • upload.root_url: Corresponds to Opts.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

  1. Create the storage directory on your server with appropriate permissions.
  2. Edit config.toml and populate the [upload] section with the values shown above.
  3. Restart Listmonk to reload configuration. The application initializes the filesystem provider during startup by reading Config.MediaUpload.Provider and constructing the store with your Opts.
  4. Verify functionality by uploading a test image through the admin UI or API; the file should appear in your configured path directory.

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 from UploadPath/filename and streams it to the client.
  • Deletion: Calling DELETE /api/media/{id} triggers the Delete() method, which removes the physical file from UploadPath and purges the database row via core.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 (where listmonk binary 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" in config.toml.
  • Define upload.path for physical storage, upload.uri for the public path prefix, and upload.root_url for the base domain.
  • The provider implementation lives in internal/media/providers/filesystem/filesystem.go, exposing standard Put, GetURL, GetBlob, and Delete methods.
  • Uploaded files are written to UploadPath/filename and served via URLs constructed as RootURL + UploadURI + filename.
  • Configuration mapping occurs in cmd/init.go, while API handlers reside in cmd/media.go and database operations in internal/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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →