# How to Configure Media Storage with Filesystem in Listmonk: A Complete Guide

> Learn how to configure Listmonk media storage using the filesystem. This guide covers setting upload provider, path, URI, and root URL for local media storage.

- Repository: [Kailash Nadh/listmonk](https://github.com/knadh/listmonk)
- Tags: how-to-guide
- Published: 2026-05-19

---

**To store uploaded campaign media on your local server, set `upload.provider = "filesystem"` in [`config.toml`](https://github.com/knadh/listmonk/blob/main/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`](https://github.com/knadh/listmonk/blob/main/internal/media/media.go). When you select the filesystem provider, Listmonk instantiates the **local store** implemented in [`internal/media/providers/filesystem/filesystem.go`](https://github.com/knadh/listmonk/blob/main/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:

```go
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`](https://github.com/knadh/listmonk/blob/main/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`](https://github.com/knadh/listmonk/blob/main/cmd/init.go) (around lines 506‑507), where `koanf` populates the `Config.MediaUpload` struct.

### Required TOML Configuration

Add the following block to [`config.toml`](https://github.com/knadh/listmonk/blob/main/config.toml):

```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:

```bash
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`](https://github.com/knadh/listmonk/blob/main/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`](https://github.com/knadh/listmonk/blob/main/cmd/media.go) receives the multipart form and calls the provider's `Put()` method:

```bash
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`](https://github.com/knadh/listmonk/blob/main/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:

```json
{
  "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`](https://github.com/knadh/listmonk/blob/main/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`](https://github.com/knadh/listmonk/blob/main/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`](https://github.com/knadh/listmonk/blob/main/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`](https://github.com/knadh/listmonk/blob/main/cmd/init.go), while API handlers reside in [`cmd/media.go`](https://github.com/knadh/listmonk/blob/main/cmd/media.go) and database operations in [`internal/core/media.go`](https://github.com/knadh/listmonk/blob/main/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.