SQLite on WSL Not Working? The Symlink Every Dev Should Know
Laravel 06/04/2026

SQLite on WSL Not Working? The Symlink Every Dev Should Know

TL;DR: SQLite on WSL fails silently due to incompatibilities in the file locking mechanism between the Linux kernel and the NTFS filesystem. The fix is to create the .sqlite file on the Windows filesystem (/mnt/c/...) and point to it via symlink from your Laravel project.


You’re Not Doing Anything Wrong

Laravel has used SQLite by default since version 11 — and for good reason. To spin up an application quickly, validate an idea, or develop a feature without depending on a locally running MySQL or PostgreSQL, SQLite is unbeatable. Zero configuration, a single file, php artisan migrate and you’re done.

But on WSL, this seemingly simple workflow sometimes turns into this:

SQLSTATE[HY000]: General error: 5 database is locked

Or worse — the database appears to work, but writes vanish, migrations hang midway, and the behavior is completely unpredictable from one run to the next. You check the .env, re-read the documentation, restart php artisan serve. Nothing changes.

The problem isn’t your application. It’s where the file lives.


The Problem: File Locking Between Two Worlds

SQLite uses a file locking mechanism to ensure write exclusivity — only one process can write to the database at a time. This mechanism relies on operating system primitives that vary across filesystems.

On WSL2, the /home directory and the entire Linux filesystem live inside a virtual disk (a .vhdx file). When SQLite tries to acquire a lock on this filesystem, it uses POSIX calls (flock, fcntl) that work correctly on native Linux.

The problem shows up in certain concurrent access scenarios — multiple workers, parallel tests, or even the development server itself with hot reload — where WSL2 cannot guarantee the locking semantics that SQLite expects. The result is a lock that never gets released, or a write that silently fails to persist.

Why does this still happen?

WSL2 is remarkably mature, but the translation layer between the Linux kernel and the underlying filesystem remains a source of friction in edge cases. SQLite has no way of knowing it’s operating on top of this layer — it tries to use the OS primitives and assumes they’ll behave predictably. When they don’t, the result is silent corruption or eternal locks.


The workaround is elegant and has zero performance cost for development use:

  1. Create the .sqlite file inside the Windows filesystem (e.g., C:\Users\samuel\databases\)
  2. Point to it via symlink from the database/ folder of your Laravel project

This way, Laravel sees the database as if it were a local file, but the actual file lives on NTFS — where SQLite can operate with functional file locking.


Step by Step

1. Create the databases folder on Windows

Open PowerShell and create a dedicated folder:

mkdir C:\Users\$env:USERNAME\databases

2. Create the SQLite file on Windows

# In the WSL terminal
touch /mnt/c/Users/samuel/databases/meu-projeto.sqlite

The file can be empty — Laravel’s migrations will populate it later.

# Navigate to the project folder
cd /home/samuel/projetos/meu-projeto
 
# Remove the existing file (if any)
rm database/database.sqlite
 
# Create the symlink
ln -s /mnt/c/Users/samuel/databases/meu-projeto.sqlite database/database.sqlite

Verify it’s set up correctly:

ls -la database/
# lrwxrwxrwx database.sqlite -> /mnt/c/Users/samuel/databases/meu-projeto.sqlite

4. No changes needed in .env

Laravel will follow the symlink transparently. The default configuration already works:

DB_CONNECTION=sqlite
# DB_DATABASE is automatically resolved to database/database.sqlite

Or, if you prefer an explicit absolute path:

DB_CONNECTION=sqlite
DB_DATABASE=/home/samuel/projetos/meu-projeto/database/database.sqlite

5. Run migrations as usual

php artisan migrate

Organizing Multiple Laravel Projects

When you work with multiple projects simultaneously — which is the most common day-to-day scenario — it’s worth structuring the Windows folder clearly:

C:\Users\samuel\databases\
├── agenda-clinica.sqlite
├── clube-leitura.sqlite
├── controle-frotas.sqlite
├── loja-artesanato.sqlite
└── tests\
    ├── agenda-clinica-test.sqlite
    ├── clube-leitura-test.sqlite
    └── controle-frotas-test.sqlite

One file per project, with descriptive names. When you need to inspect or back up a specific project’s database, everything is in one place and directly accessible through Windows Explorer — which also makes it easy to open with tools like DB Browser for SQLite or TablePlus without navigating the WSL filesystem.


Important Considerations

Laravel already ships with database/*.sqlite in .gitignore by default. Even so, make sure the symlink is also covered — in some cases Git may try to version the symbolic link itself:

# Laravel's .gitignore (already included by default, but worth confirming)
/database/*.sqlite
/database/*.sqlite-shm
/database/*.sqlite-wal

Separate test database

If you use a dedicated database for tests (a recommended practice to avoid contaminating development data), repeat the process for the test file:

ln -s /mnt/c/Users/samuel/databases/tests/meu-projeto-test.sqlite database/database-test.sqlite

And in phpunit.xml:

<env name="DB_DATABASE" value="database/database-test.sqlite"/>

For :memory: tests, this problem doesn’t exist — the database lives entirely in RAM and doesn’t depend on disk file locking. But for Laravel feature tests that need to persist data between requests, a physical file is necessary and the symlink does the job.

In production, forget this workaround

This setup is strictly for local development. In production — even if you use SQLite — the file should live on the server’s native Linux filesystem, without WSL, without network mounts, without translation layers. The workaround solves a dev environment problem; don’t carry it forward.


When This Showed Up in Practice

This problem came up for me every time I needed to spin up a Laravel application quickly using SQLite to speed up development — which is exactly the use case SQLite is meant for in Laravel. No Docker with MySQL, no configuring credentials, just php artisan migrate and move on.

The symptoms varied by project:

In a scheduling system for a clinic, migrations would occasionally hang mid-execution during the initial setup. The database would end up in an inconsistent state, and I had to delete and recreate the file on every attempt. After the symlink, the process became predictable and repeatable.

In an admin panel for a subscription club, the problem appeared in feature tests: they passed locally on an intermittent basis, but never all at once. The cause was concurrent writes in the tests that WSL’s file locking couldn’t arbitrate correctly. With the database on Windows via symlink, the tests became deterministic.

In both cases, the symptoms were different, but the cause and the solution were the same.


Why Not Move the Entire Project to /mnt/c?

It’s the obvious question. The short answer: performance.

Running a Laravel project on the Windows filesystem via WSL is significantly slower for I/O operations involving many small files — exactly what composer install, PHP’s autoloader, and php artisan itself do all the time. WSL2’s native Linux filesystem is much faster for this.

The symlink gives you the best of both worlds: the entire project lives on the fast Linux filesystem, and only the SQLite file — which needs reliable NTFS locking — sits on Windows.


Conclusion

Laravel made SQLite the default choice for a reason: it eliminates friction at the start of development. WSL2 exists for the same reason: eliminating friction for developers who work on Windows but need a Linux environment.

When the two collide on this specific point, the workaround isn’t sophisticated. It’s a symlink. But understanding why it works — the difference between the two filesystems and how SQLite depends on OS primitives to function correctly — is what turns a Stack Overflow solution into knowledge you can apply with confidence next time.

If you landed here with database is locked in your terminal at 11 PM trying to spin up a quick project, you can breathe. In two minutes you’ll be back to what matters.


Have another classic dev environment friction you solved in a non-obvious way?