n8n Data Disappearing in Docker? You're Missing a Volume Mount

Mike Holownych
Last verified: July 28, 2026
#n8n#automation
Share:

Quick Answer

n8n stores workflows, credentials, and execution history in a database — SQLite by default, at ~/.n8n/database.sqlite inside the container. If that directory isn’t mounted to a persistent Docker volume, recreating the container (not just restarting it — docker-compose down followed by up, an image update, a host migration) wipes it. Worse: if you never explicitly set N8N_ENCRYPTION_KEY, n8n auto-generates one and stores it in that same unmounted directory — so even a database backup you took separately becomes undecryptable once the key is gone. The fix is a persistent volume for /home/node/.n8n and an explicitly-set, separately-backed-up encryption key.

Why “It Worked Before the Restart” Is Misleading

A plain docker restart on an existing container doesn’t lose data — the container’s filesystem, including ~/.n8n, survives a restart intact. What actually causes the data loss people hit is container recreation: docker-compose down && up, docker rm followed by a fresh run, or redeploying to a new host — any operation that discards the old container and starts a new one from the image. A fresh container has a fresh, empty ~/.n8n unless that path is bound to a volume that persists independently of the container’s lifecycle.

The credentials piece compounds this. Credentials are stored encrypted in the same database, using N8N_ENCRYPTION_KEY. If you never set that variable explicitly, n8n generates one on first startup and writes it to ~/.n8n/config — inside the same directory that just got wiped. Restore a database backup without that exact key, and every stored credential is permanently undecryptable garbage, even though the backup itself is intact.

The Fix: Persistent Volume + Explicit Encryption Key

Step 1: Environment Configuration

# Database (use Postgres for production; see below)
DB_TYPE=postgresdb
DB_POSTGRESDB_HOST=localhost
DB_POSTGRESDB_PORT=5432
DB_POSTGRESDB_DATABASE=n8n
DB_POSTGRESDB_USER=n8n_user
DB_POSTGRESDB_PASSWORD=secure_password_here

# Set this explicitly and back it up independently of the database.
# Never change it after first use — existing credentials become
# undecryptable if the key changes.
N8N_ENCRYPTION_KEY=your-32-character-encryption-key-here

WEBHOOK_URL=https://your-domain.com/
N8N_PROTOCOL=https
N8N_HOST=your-domain.com
N8N_PORT=5678

Generate a key once with openssl rand -hex 16, store it in your secrets manager, and never let n8n auto-generate one for you.

Step 2: Docker Volume Mapping

version: '3.7'
services:
  n8n:
    image: n8nio/n8n:latest
    restart: always
    ports:
      - "5678:5678"
    environment:
      - DB_TYPE=postgresdb
      - DB_POSTGRESDB_HOST=postgres
      - DB_POSTGRESDB_DATABASE=n8n
      - DB_POSTGRESDB_USER=n8n_user
      - DB_POSTGRESDB_PASSWORD=${DB_PASSWORD}
      - N8N_ENCRYPTION_KEY=${ENCRYPTION_KEY}
    volumes:
      - n8n_data:/home/node/.n8n
    depends_on:
      - postgres

  postgres:
    image: postgres:13
    restart: always
    environment:
      - POSTGRES_DB=n8n
      - POSTGRES_USER=n8n_user
      - POSTGRES_PASSWORD=${DB_PASSWORD}
    volumes:
      - postgres_data:/var/lib/postgresql/data

volumes:
  n8n_data:
  postgres_data:

n8n_data covers the .n8n directory (config, encryption key if not overridden, any local files). Moving the database itself to Postgres — with its own volume — separates the two: a container recreation no longer touches your workflow and execution data at all, since it lives in Postgres rather than inside the n8n container’s filesystem.

Step 3: Back Up Credentials Independently

Don’t rely on curling n8n’s internal REST API for credential export — use the CLI, which is n8n’s actual supported mechanism:

n8n export:credentials --all --output=credentials-backup.json --decrypted=false

Keep credentials-backup.json and your N8N_ENCRYPTION_KEY backed up in separate locations. Either one alone is useless without the other, which is the point — but it also means you need both, recoverable, for a real restore.

Step 4: Verify Persistence Actually Works

Before trusting this in production, prove it:

# Note the workflow count
curl -s "http://localhost:5678/rest/workflows" | jq '. | length'

# Force a full container recreation, not just a restart
docker-compose down
docker-compose up -d

# Count should be identical
curl -s "http://localhost:5678/rest/workflows" | jq '. | length'

If the count drops to zero, the volume mount isn’t working — check that the path inside the container matches exactly and that the named volume was actually created (docker volume ls).

Three Mistakes That Cause This

Assuming a restart and a recreation are the same operation. They aren’t — a restart is safe on an existing container; recreation without a volume mount is what loses data. If your deploy process ever runs down/up or replaces containers (most CI/CD pipelines do), you need the volume regardless of how stable your uptime has looked so far.

Letting the encryption key auto-generate. It works fine until the moment you need to restore a backup on a different host or after a wipe, at which point an auto-generated key you never saved makes every stored credential permanently unusable. Set it explicitly from day one.

Running SQLite in a container with no volume, ever, even for testing that later became “temporarily” production. Test setups have a way of becoming the real deployment. Configure persistence before that happens, not after the first incident.

See n8n’s Default SQLite Database Isn’t Built for Production for the broader case on why Postgres is the better production default, and n8n Enterprise Architecture for the full production-readiness picture.

MH

About Mike Holownych

Building AI Syndicate—governance infrastructure for AI agents in regulated environments. 20+ years enterprise operations, now applying that reliability discipline to AI deployment.