> ## Content Index
> Fetch the complete content index at: https://anantafatur.dev/llms.txt
> Use this file to discover other available public pages before exploring further.

# When Docmost says "Error uploading file to drive"
- URL: https://anantafatur.dev/docmost-error-uploading-file-to-drive/
- Published: 2026-09-18T19:00:55.000Z
- Updated: 2026-09-18T19:00:55.000Z
- Description: Tracing a misleading error message in Docmost back to a Docker bind mount permission issue, and a permanent fix using a custom entrypoint script.
- Author: Ananta

I was trying to upload a file attachment to a Docmost page and got hit with a generic error: "Error uploading file to drive." No stack trace, no permissions hint, no file path. Just a plain 400 Bad Request. The UI was equally unhelpful, showing the same message without any clue about what went wrong.

This post walks through how I traced the real cause and how I fixed it permanently.

## The symptoms

A file upload to a Docmost page returned a 400 error. Here is the full log:

```
"context":"AttachmentService","msg":"Error uploading file to drive:"
"context":"AttachmentController","err":{"type":"BadRequestException","message":"Error uploading file to drive",...}

```

Two things stood out immediately. First, the `AttachmentService` log ends with a colon and nothing after it. That suggests the error object was logged but its string representation was empty. Second, the controller throws a `BadRequestException` with the same static message, which means whatever the real error was, it got replaced with a canned string.

The error is clearly being swallowed. Somewhere between the storage driver throwing an exception and the controller returning a response, the real error message is lost.

---

## The environment

I run Docmost in Docker using `docker compose`. The setup is simple: a Postgres database, a Redis instance, and the Docmost app itself. File storage uses the default `local` driver, meaning uploaded files go to the container filesystem at `/app/data/storage`. That path is mounted as a bind mount from a directory on the host.

Docmost version is 0.96.0, pulled from the official Docker image. The container runs as user `node` (uid 1000), which is the default in the upstream image.

I have a custom Dockerfile on top of the official image that applies a few npm package patches. The `docker-compose.yml` had the build section commented out, so it was using the vanilla image directly.

## Tracing the error

I grabbed the error log and sent it to DeepSeek V4 Flash. I am not a Node.js developer and I did not want to spend time reading compiled JavaScript inside a container. The AI had the full picture from the log alone.

It started by reading the compiled source from inside the running container. This is what it found in the `uploadToDrive` method inside `attachment.service.js`:

```javascript
async uploadToDrive(filePath, fileContent) {
    try {
        await this.storageService.upload(filePath, fileContent);
    } catch (err) {
        this.logger.error('Error uploading file to drive:', err);
        throw new BadRequestException('Error uploading file to drive');
    }
}

```

The catch block logs `err` directly. If `err` is not a standard Error object, the NestJS Logger might serialize it as an empty string, which explains the colon with nothing after it in the log. Then it throws a new `BadRequestException` with a hardcoded message, completely burying the real cause.

The AI went deeper. It checked the `storage.service.js` file and found that `upload` delegates to a storage driver. Since no `STORAGE_DRIVER` env var was set, it defaults to `local`. The local driver's upload method looked like this:

```javascript
async upload(filePath, file) {
    try {
        const fullPath = this._fullPath(filePath);
        if (file instanceof Buffer) {
            await fs.outputFile(fullPath, file);
        } else {
            await fs.mkdir(dirname(fullPath), { recursive: true });
            await pipeline(file, createWriteStream(fullPath));
        }
    } catch (err) {
        throw new Error(`Failed to upload file: ${err.message}`);
    }
}

```

For file uploads, the code creates the directory with `mkdir` and then pipes the stream to a write stream. The chain was clear: `mkdir` fails -> local driver wraps the error -> `storageService.upload` propagates it -> `uploadToDrive` logs and re-throws -> controller returns 400.

That pointed to a filesystem permission problem. The AI ran a quick check inside the container:

```bash
$ docker exec docmost-app ls -la /app/data/storage/
total 12
drwxr-xr-x 3 root root 4096 Apr 30 04:08 .
drwxr-xr-x 3 node node 4096 Sep  8 15:49 ..
drwxr-xr-x 3 root root 4096 Apr 30 04:08 <workspace-id>

```

The storage directory and its subdirectories were owned by `root` with permissions `755`. The app runs as `node` (uid 1000), so it could read and traverse directories but could not create new files or directories inside them. The `mkdir` call was hitting `EACCES`.

## Why were the permissions wrong?

The container had been running with a different configuration at some point -- perhaps before I added `USER node` to the custom Dockerfile, or perhaps an earlier version of the upstream image ran as root. When files were first written to the bind-mounted volume, they were created with root ownership. After switching to the `node` user, the app could no longer write to those directories.

Fun fact: existing files from earlier uploads were still readable because the `r-x` permission on directories was enough for reading. Only new uploads broke.

This also explains why the error was hard to debug. The app had been working fine for previous uploads. The breakage only showed up on new file writes, and the error message gave no hint about permissions.

---

## The fix

The AI proposed both an immediate fix and a permanent one.

### Immediate fix

Change the ownership of the storage directory to match the user the app runs as:

```bash
docker exec -u root docmost-app chown -R node:node /app/data/storage

```

This runs `chown` as root inside the container and changes ownership of the entire storage tree to `node:node`. I ran this and uploads started working again.

But this is a manual fix. If the container is recreated or if the volume is mounted on a new host, the permissions would be wrong again.

### Permanent fix

The AI created a custom entrypoint script that runs `chown` at container startup before dropping privileges and running the application.

Here is the entrypoint script (`entrypoint.sh`):

```bash
#!/bin/sh
set -e

# Ensure the storage directory is writable by the node user
chown -R node:node /app/data/storage

# Drop privileges to node user and run the original entrypoint
exec su -s /bin/sh -c "exec /docker-entrypoint.sh $*" node

```

The script runs as root because `USER node` was removed from the end of the Dockerfile. It fixes the permissions, then uses `su` to switch to `node` and execute the original entrypoint that ships with the Docmost image.

The AI updated the Dockerfile to copy this script and set it as the entrypoint:

```dockerfile
COPY entrypoint.sh /entrypoint-wrapper.sh
RUN chmod +x /entrypoint-wrapper.sh

ENTRYPOINT ["/entrypoint-wrapper.sh"]
CMD ["pnpm", "start"]

```

It also uncommented the `build` section in `docker-compose.yml` so the container builds from the custom Dockerfile instead of pulling the vanilla image.

After rebuilding and restarting, the permissions are now fixed automatically on every container start. I did not have to think about any of this – the AI handled the code changes, I just ran the rebuild command.