This error occurs when SQLite cannot access a database file or directory due to insufficient file system permissions. Common causes include incorrect ownership, restrictive file modes, or attempting to write to read-only locations.
SQLITE_PERM is a file system permission error that occurs when SQLite lacks the necessary access rights to perform an operation on a database file or its parent directory. This error code (SQLITE_PERM = 3) indicates that the operating system denied the requested access mode. SQLite requires specific permissions depending on the operation: 1. **Read operations**: Require read permission on the database file 2. **Write operations**: Require write permission on both the database file AND its parent directory 3. **WAL mode**: Requires the ability to create auxiliary files (.wal, .shm) in the same directory 4. **Journal mode**: Requires permission to create temporary journal files The error is particularly common when: - Running applications under restrictive user accounts (like web servers) - Database files are created by one user but accessed by another - Deploying applications to production environments with different permission models - Using containerized applications where volume permissions may not match expectations
First, examine the permissions on your database file and its parent directory:
On Linux/macOS:
# Check database file permissions
ls -la /path/to/database.db
# Check parent directory permissions
ls -ld /path/to/
# Check auxiliary files if they exist
ls -la /path/to/database.db-*On Windows (PowerShell):
# Check file permissions
Get-Acl "C:\path\to\database.db" | Format-List
# Check directory permissions
Get-Acl "C:\path\to\" | Format-ListLook for:
- File ownership (user:group)
- Read/write/execute permissions
- Whether your application's user has access
Grant appropriate permissions to the database file and directory:
For Linux/macOS (single user):
# Make the database file readable and writable by owner
chmod 644 /path/to/database.db
# Ensure the directory is writable (needed for journal/WAL files)
chmod 755 /path/to/
# If you need group access (e.g., for web server)
chmod 664 /path/to/database.db
chmod 775 /path/to/For web applications (Linux):
# Change ownership to web server user
sudo chown www-data:www-data /path/to/database.db
sudo chown www-data:www-data /path/to/
# Set appropriate permissions
sudo chmod 660 /path/to/database.db
sudo chmod 770 /path/to/For Windows (IIS applications):
# Grant IIS_IUSRS read/write access
$acl = Get-Acl "C:\path\to\database.db"
$permission = "IIS_IUSRS", "FullControl", "Allow"
$accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule $permission
$acl.SetAccessRule($accessRule)
Set-Acl "C:\path\to\database.db" $acl
# Also grant access to the directory
$acl = Get-Acl "C:\path\to\"
$acl.SetAccessRule($accessRule)
Set-Acl "C:\path\to\" $aclDetermine which user your application is actually running as:
For web applications (Linux):
# Check Apache user
ps aux | grep apache2
# or
ps aux | grep httpd
# Check Nginx user
ps aux | grep nginx
# Check Node.js application user
ps aux | grep nodeFor containerized applications:
# Check the user inside the container
docker exec <container-name> whoami
# Check the UID/GID
docker exec <container-name> idFor Windows services:
# Check service account
Get-WmiObject win32_service | Where-Object {$_.Name -eq 'YourServiceName'} | Select Name, StartNameOnce you know the user, ensure that user has appropriate permissions on the database file and directory.
If running in Docker, ensure volume permissions match the container user:
Option 1: Match container user to host UID/GID
# In your Dockerfile
FROM node:18
RUN useradd -m -u 1000 -s /bin/bash appuser
USER appuser# In docker-compose.yml
services:
app:
volumes:
- ./data:/app/data
user: "1000:1000" # Match host user UID:GIDOption 2: Fix permissions at runtime
# In your Dockerfile or entrypoint script
RUN chown -R appuser:appuser /app/dataOption 3: Use named volumes with proper initialization
# In docker-compose.yml
volumes:
sqlite_data:
driver: local
services:
app:
volumes:
- sqlite_data:/app/dataVerify permissions inside the container:
docker exec <container-name> ls -la /app/data
docker exec <container-name> touch /app/data/test.txtOn systems with mandatory access control, additional configuration may be needed:
For SELinux (Red Hat, CentOS, Fedora):
# Check SELinux status
sestatus
# Check context of database file
ls -Z /path/to/database.db
# Allow httpd to write to database directories
sudo setsebool -P httpd_unified 1
# Or set the correct context manually
sudo chcon -t httpd_sys_rw_content_t /path/to/database.db
sudo chcon -t httpd_sys_rw_content_t /path/to/For AppArmor (Ubuntu, Debian):
# Check AppArmor status
sudo aa-status
# If needed, set profile to complain mode
sudo aa-complain /etc/apparmor.d/usr.sbin.nginx
# Or add exceptions to the profile
sudo nano /etc/apparmor.d/local/usr.sbin.nginx
# Add: /path/to/database.db rw,
sudo systemctl reload apparmorTemporarily disable to test (not recommended for production):
# SELinux
sudo setenforce 0
# AppArmor
sudo systemctl stop apparmorIf the database is in a restricted system directory, move it to a more appropriate location:
For Linux applications:
# Create a data directory with proper permissions
sudo mkdir -p /var/lib/myapp/data
sudo chown www-data:www-data /var/lib/myapp/data
sudo chmod 750 /var/lib/myapp/data
# Move the database
sudo mv /restricted/path/database.db /var/lib/myapp/data/
sudo chown www-data:www-data /var/lib/myapp/data/database.dbFor Windows applications:
# Use application data directory
$dataPath = "$env:ProgramData\MyApp\Data"
New-Item -ItemType Directory -Force -Path $dataPath
# Move and set permissions
Move-Item "C:\Program Files\MyApp\database.db" $dataPath
icacls $dataPath /grant "IIS_IUSRS:(OI)(CI)M"Recommended locations by platform:
- Linux: /var/lib/myapp/, /opt/myapp/data/, or ~/.local/share/myapp/
- Windows: %ProgramData%\MyApp\, %AppData%\MyApp\
- macOS: ~/Library/Application Support/MyApp/, /usr/local/var/myapp/
Update your application configuration to point to the new location.
Create a simple test to verify permissions are working:
Test script (test-sqlite-permissions.sh):
#!/bin/bash
DB_PATH="/path/to/database.db"
DB_DIR=$(dirname "$DB_PATH")
echo "Testing SQLite permissions..."
echo ""
# Test directory write
echo "1. Testing directory write permission..."
touch "$DB_DIR/test_file.tmp" 2>/dev/null && rm "$DB_DIR/test_file.tmp" && echo "✓ Directory writable" || echo "✗ Directory NOT writable"
# Test database read
echo "2. Testing database read permission..."
sqlite3 "$DB_PATH" "SELECT 1;" 2>/dev/null && echo "✓ Database readable" || echo "✗ Database NOT readable"
# Test database write
echo "3. Testing database write permission..."
sqlite3 "$DB_PATH" "CREATE TABLE IF NOT EXISTS perm_test (id INTEGER); DROP TABLE IF EXISTS perm_test;" 2>/dev/null && echo "✓ Database writable" || echo "✗ Database NOT writable"
# Check WAL files can be created
echo "4. Testing WAL mode..."
sqlite3 "$DB_PATH" "PRAGMA journal_mode=WAL; PRAGMA journal_mode;" 2>/dev/null && echo "✓ WAL mode works" || echo "✗ WAL mode FAILED"
echo ""
echo "Current permissions:"
ls -la "$DB_PATH"
ls -ld "$DB_DIR"Run as the application user:
# Run as specific user
sudo -u www-data bash test-sqlite-permissions.sh
# Or in Docker
docker exec -u www-data <container-name> /path/to/test-sqlite-permissions.sh### Understanding SQLite Permission Requirements
SQLite's permission requirements are more complex than typical file access because of its transaction mechanisms:
Why directory write permission is required:
When SQLite performs a write transaction, it creates auxiliary files:
- Rollback journal mode: Creates a .db-journal file
- WAL mode: Creates .db-wal and .db-shm files
These files must be in the same directory as the database file, which is why directory write permission is essential even for database writes.
Permission matrix:
| Operation | Database File | Parent Directory | Notes |
|-----------|---------------|------------------|-------|
| Read-only SELECT | Read | - | No directory access needed |
| Write (rollback) | Read + Write | Write | Journal file creation |
| Write (WAL mode) | Read + Write | Write | WAL/SHM file creation |
| CREATE DATABASE | - | Write | New file creation |
### Security Considerations
Minimum privilege principle:
# Database file: Owner can read/write, group can read, others nothing
chmod 640 database.db
# Directory: Owner can list/enter/write, group can list/enter, others nothing
chmod 750 /path/to/data/Avoid these insecure patterns:
- chmod 777 - Never make database world-writable
- Storing databases in web-accessible directories
- Running applications as root just to fix permissions
- Using chown -R on system directories
Use dedicated database directories:
Create a specific directory for SQLite databases with appropriate ownership and permissions, separate from application code and web content.
### Platform-Specific Gotchas
Linux systemd services:
Services run with DynamicUser=true may have unexpected user IDs. Check with:
systemctl show -p User,UID myservice.serviceWindows WSL:
File permissions on Windows drives (/mnt/c/) may not behave as expected. Store databases on the Linux file system (/home/) for proper permission support.
macOS code signing:
Signed applications may have restricted access to certain directories. Use appropriate sandboxing entitlements or store databases in user-accessible locations.
### Debugging Permission Errors
Enable SQLite error logging:
// In C/C++ applications
int sqlite3_config(SQLITE_CONFIG_LOG, errorLogCallback, NULL);Use strace/dtrace to see system calls:
# Linux
strace -e trace=open,openat,access sqlite3 database.db 2>&1 | grep EACCES
# macOS
sudo dtruss -t open sqlite3 database.db 2>&1 | grep "Err#13"Check audit logs:
# SELinux denials
sudo ausearch -m avc -ts recent
# AppArmor denials
sudo journalctl | grep DENIED### WAL Mode and Permissions
WAL mode has additional permission implications:
1. Shared memory file (.db-shm) requires reader and writer coordination
2. Multiple processes must all have access to create/read/write WAL files
3. Permission errors in WAL mode may appear as SQLITE_BUSY instead of SQLITE_PERM
If permission issues persist in WAL mode, try:
PRAGMA journal_mode=DELETE; -- Revert to rollback journal### Docker Best Practices
Create a dedicated user in your Dockerfile:
FROM alpine:latest
# Create app user with specific UID/GID
RUN addgroup -g 1000 appgroup && \
adduser -D -u 1000 -G appgroup appuser
# Create data directory with correct ownership
RUN mkdir -p /app/data && \
chown -R appuser:appgroup /app/data
USER appuser
WORKDIR /appFor existing containers, fix permissions in entrypoint:
#!/bin/sh
# entrypoint.sh
chown -R $(id -u):$(id -g) /app/data
exec "$@"SQLITE_BUSY: The database file is locked
How to fix 'SQLITE_BUSY: The database file is locked' in SQLite
better-sqlite3: This statement has already been finalized
How to fix "better-sqlite3: This statement has already been finalized" in SQLite
SQLITE_AUTH: Authorization denied
SQLITE_AUTH: Authorization denied
SQLITE_CONSTRAINT_CHECK: CHECK constraint failed
CHECK constraint failed in SQLite
SQLITE_READONLY_RECOVERY: WAL mode database cannot be modified
How to fix "SQLITE_READONLY_RECOVERY: WAL mode database cannot be modified" in SQLite