Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

SQLite for Production Small Sites and CLI

By Kokil Thapa | Last reviewed: September 2026

You do not always need MySQL running on a second port. SQLite for production small sites and CLI is a valid choice when traffic stays modest, the app runs on one server, and your team wants fewer moving parts. I have shipped brochure sites, internal tools, and CLI scripts this way. The database is one file on disk. No daemon. No connection pool drama. That simplicity cuts hosting cost and nightly ops work. It also sets hard ceilings you must respect. This guide covers when SQLite fits, how to configure it in Laravel and PHP, backup patterns from the shell, and the point where you should migrate to PostgreSQL or MySQL for production.

When should you use SQLite for production small sites and CLI?

SQLite shines where a full RDBMS server would be overkill. Think law-firm landing pages with a contact log, a one-person SaaS admin panel, or a Deployer hook that records release metadata. On sister sites I maintain with Laravel scheduled tasks in production, a SQLite file often backs lightweight reporting tables that never leave the VPS.

Good candidates share these traits:

  • Single application server (or container) owns all writes
  • Mostly read-heavy pages; writes are infrequent and short
  • Traffic under roughly a few thousand page views per day
  • No requirement for remote DB replicas or multi-app shared writes
  • Team prefers simple database backup strategies for small servers over replication tooling

Poor candidates include multi-tenant carts, real-time chat, heavy background job fan-out, or any design that expects dozens of concurrent writers. SQLite allows one writer at a time per database file. Readers can proceed in WAL mode, but writers queue. That is fine for a contact form. It is not fine for an order pipeline during a sale.

SQLite on a Single VPSWeb AppLaravel / PHPCLI ScriptsArtisan / Bashdatabase.sqliteWAL + SHM sidecarsOne writer lockNightlyBackuprsync / S3No separate database daemon — file lives beside your appIdeal for small brochure sites, tools, and ops scripts
SQLite for production small sites: one database file on the same server as the web app and CLI jobs

For a public legal-guide site like Court Marriage In Nepal, SQLite would only work if lead volume stays low and you run a single VPS. Most client portals I build instead use MySQL because document uploads, payments, and concurrent staff logins arrive early. Be honest about the next six months, not just launch week.

How do you configure SQLite for production in Laravel?

Laravel treats SQLite as a first-class driver. Point DB_CONNECTION at sqlite, place the file outside the public web root, and run migrations normally. PHP 8.3 or 8.5 ships with PDO SQLite enabled on typical Ubuntu images. Laravel 12 and 13 both support this path; you do not need a database server package on the host.

Set environment variables

DB_CONNECTION=sqlite
DB_DATABASE=/var/www/myapp/shared/storage/database/database.sqlite

Keep the file in a persistent shared directory if you deploy with symlinked releases. On Deployer-style setups, the same pattern I use on off-site backup pipelines to S3, only storage/ and .env survive between releases. Never store the database under public/.

Enable WAL mode and pragmas

Default rollback journal mode blocks readers during writes. WAL (Write-Ahead Logging) lets readers and one writer overlap. That matters even on small sites when a cron job writes analytics while visitors browse.

Run this once after creating the database, or from a migration:

DB::statement('PRAGMA journal_mode=WAL;');
DB::statement('PRAGMA synchronous=NORMAL;');
DB::statement('PRAGMA busy_timeout=5000;');
DB::statement('PRAGMA foreign_keys=ON;');

synchronous=NORMAL trades a tiny durability edge for much better write speed on VPS disks. For a marketing site that is usually acceptable. For financial ledger data, stay on FULL and reconsider MySQL. The official WAL documentation is at sqlite.org/wal.html.

WAL Mode Write FlowHTTP ReaderHTTP ReaderQueue Workerdatabase.sqlitemain file-wal fileappend logWriterlockMany readers OK — only one writer at a timebusy_timeout reduces "database is locked" errors
WAL mode lets SQLite serve concurrent reads while serializing writes for production small sites

File permissions on Linux

PHP-FPM runs as www-data. Cron and CLI may run as deploy or root. Mismatch causes silent write failures.

  1. Create the file ahead of deploy: touch /var/www/myapp/shared/storage/database/database.sqlite
  2. Set group ownership: chown deploy:www-data database.sqlite
  3. Allow group read/write: chmod 664 database.sqlite
  4. Ensure the directory is group-writable so WAL files can appear

This mirrors permission fixes I apply during Linux system administration for PHP sites. Wrong ownership is a common post-deploy surprise when SQLite replaces MySQL on a small VPS.

Connection settings in config/database.php

'sqlite' => [
    'driver' => 'sqlite',
    'url' => env('DB_URL'),
    'database' => env('DB_DATABASE', database_path('database.sqlite')),
    'prefix' => '',
    'foreign_key_constraints' => true,
    'busy_timeout' => 5000,
    'journal_mode' => 'wal',
    'synchronous' => 'normal',
],

Laravel passes these options to PDO on connect. See the Laravel database documentation for driver-specific keys. Test migrations on staging that mirrors production file paths, as described in our guide to staging environments that mirror production.

What are the backup and concurrency limits of SQLite in production?

SQLite backups are file copies, not logical dumps. That is simpler until someone copies a live file mid-write and gets corruption. Use SQLite's online backup API or the .backup command so copies are consistent.

Safe CLI backup with sqlite3

sqlite3 /var/www/myapp/shared/storage/database/database.sqlite \
  ".backup '/backups/myapp-$(date +%F).sqlite'"
gzip "/backups/myapp-$(date +%F).sqlite"

Schedule that from cron after low-traffic hours in Nepal (often 02:00 NPT). Push the gzip to object storage using the same mindset as WordPress automated backups with WP-CLI. Keep seven daily and four weekly copies minimum. A Rs 500/month (~USD 3.70) object storage bucket is cheap insurance.

Production Backup PathLive DBWAL active.backupconsistent copygzipS3 / SFTPoff-siteNever copy database.sqlite with cp while app is writingUse .backup or sqlite3 backup API for hot copies
Safe SQLite backup chain for production small sites: online backup, compress, store off-site

Concurrency and size ceilings

SQLite handles databases up to terabytes on paper. In practice, keep production files under a few gigabytes for sane backup times. Watch these signals:

  • database is locked errors even with busy_timeout
  • Queue workers and HTTP requests fighting over long transactions
  • Backup windows exceeding available disk I/O during business hours
  • Need for a separate reporting replica or BI tool connection

When any of those appear, start a migration plan. Website migration projects often include exactly this jump from file DB to managed MySQL. Do it before Dashain traffic spikes if you serve Nepal retail audiences.

How do you use SQLite from the command line for ops tasks?

The sqlite3 binary is where SQLite earns the CLI half of this topic. Ops scripts, one-off imports, and deploy smoke tests do not need the full app booted.

Inspect schema and row counts

sqlite3 database.sqlite ".tables"
sqlite3 database.sqlite "SELECT COUNT(*) FROM leads;"
sqlite3 -header -column database.sqlite "SELECT id, email, created_at FROM leads LIMIT 5;"

Pretty output helps when you SSH into a client VPS and need answers in thirty seconds. Pipe JSON through your own script, or paste results into the JSON formatter tool if you export query results for a ticket.

Run parameterized imports

sqlite3 database.sqlite <<'SQL'
BEGIN;
INSERT INTO tags (name, slug) VALUES ('news', 'news');
INSERT INTO tags (name, slug) VALUES ('guide', 'guide');
COMMIT;
SQL

Wrap bulk changes in transactions. SQLite commits are fast in WAL mode, but one row per commit on a thousand rows is painfully slow on HDD VPS plans common at Rs 800–1,500/month (~USD 6–11).

Artisan and SQLite together

php artisan migrate --force
php artisan db:show
php artisan tinker --execute="echo DB::table('users')->count();"

Use --force in production deploy hooks. I wire this into GitLab CI after symlink swap, same pattern as PHP-FPM reload on legal-tech sister sites. For heavier reporting, export CSV:

sqlite3 -header -csv database.sqlite "SELECT * FROM page_views;" > /tmp/views.csv

Feed that CSV into a spreadsheet or a log aggregation setup for small teams if you outgrow manual review but are not yet on MySQL.

SQLite vs MySQL: which fits small production sites?

Both run fine on a $5–15 VPS. The difference is operational shape, not raw SQL syntax.

CriteriaSQLiteMySQL 8.4 LTS
Process overheadNone — embedded librarymysqld daemon + memory buffer pool
Concurrent writersOne per database fileMany with row-level locking
Remote app accessFile path or custom API onlyStandard TCP host/port
Backup storyFile backup via .backupmysqldump or Percona XtraBackup
Hosting on shared VPSExcellent — zero extra serviceGood — needs tuning and monitoring
Typical small-site fitBrochure, micro-SaaS, CLI toolingeCommerce, portals, multi-user CRM
Laravel queue + HorizonWorks with sync/database driverPreferred with Redis + MySQL metadata

Verdict: pick SQLite when you want the smallest ops surface and can accept single-writer limits. Pick MySQL when staff log in together, payments land, or you already plan e-commerce development features. A travel brochure site like Adventure Himalaya Nepal could start on SQLite; a booking CRM with supplier tables should not.

SQLite vs MySQL DecisionNew small site?Single server?Multi app DB?Use SQLiteUse MySQLUse MySQLLow writesHeavy queuesShared accessRe-evaluate when staff, payments, or traffic grow
Decision tree for SQLite for production small sites and CLI versus moving to MySQL

PHP without Laravel

Plain PHP scripts on PHP 8.5 work the same way through PDO:

$pdo = new PDO('sqlite:' . __DIR__ . '/data/app.sqlite');
$pdo->exec('PRAGMA journal_mode=WAL');
$pdo->exec('PRAGMA foreign_keys=ON');
$stmt = $pdo->prepare('INSERT INTO events (name) VALUES (:name)');
$stmt->execute(['name' => 'deploy']);

The PHP manual covers driver quirks at php.net PDO SQLite. This pattern suits deploy hooks and health checks where bootstrapping Laravel adds seconds you do not need.

Monitoring and testing

SQLite lacks SHOW PROCESSLIST. Monitor file size, lock errors in logs, and page latency instead. Add an artisan command or shell probe:

sqlite3 database.sqlite "PRAGMA integrity_check;"

Run weekly. Pair with application tests on every deploy. For teams building custom internal tools, custom software development engagements should document the migration trigger in the README so the next developer knows when to leave SQLite.

Cost angle matters for Nepal startups. Skipping MySQL saves roughly 200–400 MB RAM on a 1 GB VPS. That is meaningful at Rs 1,200/month hosting tiers. It is meaningless once lost orders cost more than the server bill. See cloud cost optimization for small startups for the wider picture.

Feature flags and gradual rollouts still apply. SQLite does not change how you ship code. You can toggle read-heavy modules first, as in feature flag rollout for small teams, before you split read replicas on MySQL later.

If you containerize, mount the database file on a persistent volume. Ephemeral container layers wipe SQLite on restart. Our Docker multi-stage Laravel guide covers volume placement. Without it, you will restore from backup on every redeploy.

Ongoing care belongs in a support plan. File permissions drift after manual edits. WAL files grow until checkpointed. Include SQLite checks in support and maintenance retainers the same way you would run mysqlcheck on larger stacks.

Key Takeaways

  • Use SQLite for production small sites and CLI when one server, low write concurrency, and simple backups match your traffic reality.
  • Enable WAL mode, busy_timeout, and foreign keys; store the file outside public/ with correct www-data permissions.
  • Back up with sqlite3 .backup, never raw cp on a live file; gzip and push off-site nightly.
  • Watch for lock errors, growing file size, and multi-user write needs — those are your MySQL migration triggers.
  • CLI ops (sqlite3, Artisan, CSV export) stay fast for debugging without booting a database server.
  • Document the upgrade path early so a successful small site does not stall on a file database past its limits.

People Also Ask

Is SQLite safe for production websites?

Yes, for small production sites with a single writer pattern and proper WAL configuration. High-traffic eCommerce, multi-user dashboards, and apps with many concurrent writes should use MySQL or PostgreSQL instead. Safety depends on workload fit, not the label "embedded database."

Can Laravel use SQLite in production?

Laravel 12 and 13 support SQLite in production through the standard sqlite driver. Run migrations, set pragmas, fix file permissions, and use the database queue driver only if job volume stays low. Most production Laravel apps still choose MySQL when Horizon, Sanctum token churn, or team scale enters the picture.

How do you back up SQLite without downtime?

Use the sqlite3 interactive .backup command or the online backup API. Both produce a consistent snapshot while the app serves traffic in WAL mode. Compress the result and store copies off-server. Test a restore quarterly.

When should you migrate from SQLite to MySQL?

Migrate when you see repeated lock timeouts, need remote database access from multiple apps, run heavy write queues, or require replication and point-in-time recovery. Planning the move before peak season beats emergency migration under load.

Ship small, plan the upgrade path

SQLite for production small sites and CLI is not a shortcut for every project. It is the right default when your app, your VPS, and your ops time are all small on purpose. Configure WAL, automate consistent backups, and write down the traffic signals that mean MySQL. If you want a second opinion on whether your brochure site, legal portal, or internal tool fits SQLite or needs a full database server, contact us or browse web development services and the portfolio for sites that started lean and scaled when the business asked.

Frequently Asked Questions

Yes, for small production sites with a single writer pattern and proper WAL configuration. High-traffic eCommerce, multi-user dashboards, and apps with many concurrent writes should use MySQL or PostgreSQL instead.

Yes. Laravel 12 and 13 support SQLite in production through the standard sqlite driver. Run migrations, set pragmas, fix file permissions, and keep job volume low if using the database queue driver.

Use the sqlite3 .backup command or the online backup API. Both produce a consistent snapshot while the app serves traffic in WAL mode. Compress with gzip and store copies off-server.

SQLite fits when one server owns all writes, traffic stays under a few thousand page views per day, and writes are infrequent. I have shipped brochure sites, internal tools, and CLI scripts this way. Good candidates include law-firm landing pages with contact logs, one-person SaaS admin panels, and Deployer hooks recording release metadata. Poor candidates include multi-tenant carts, real-time chat, heavy background job fan-out, or any design expecting dozens of concurrent writers. Be honest about the next six months, not just launch week.

Set DB_CONNECTION=sqlite and DB_DATABASE to a path outside public/, ideally in a persistent shared directory such as /var/www/myapp/shared/storage/database/database.sqlite on Deployer-style deployments where only storage/ and .env survive between releases. In config/database.php, enable foreign_key_constraints, busy_timeout of 5000, journal_mode wal, and synchronous normal. Run migrations normally on PHP 8.3 or 8.5 with PDO SQLite enabled. Test on staging that mirrors production file paths before going live.

Enable WAL mode so readers and one writer can overlap instead of blocking during writes. Set synchronous=NORMAL for better write speed on VPS disks, though financial ledger data should stay on FULL with MySQL instead. Set busy_timeout=5000 to wait five seconds before returning a database is locked error. Enable foreign_keys=ON for referential integrity. Run these once after creating the database or from a migration using DB::statement calls. Default rollback journal mode blocks readers during writes, which hurts even small sites when cron writes analytics while visitors browse.

Never under public/. Place it in a persistent shared directory that survives symlinked releases, such as shared/storage/database/ on Deployer setups. The file must sit outside the web root and allow WAL sidecar files to be created in the same directory. If you containerize, mount the database on a persistent volume because ephemeral container layers wipe SQLite on every restart. Storing under public/ exposes the file to direct HTTP access and is a basic security mistake on any production VPS.

PHP-FPM runs as www-data while cron and CLI may run as deploy or root. Create the file ahead of deploy with touch, set group ownership with chown deploy:www-data, and chmod 664 for group read/write. Ensure the parent directory is group-writable so WAL and SHM files can appear. Wrong ownership is a common post-deploy surprise when SQLite replaces MySQL on a small VPS and causes silent write failures. This mirrors permission fixes I apply regularly during Linux system administration for PHP sites.

Never copy a live file with raw cp mid-write because that risks corruption. Use sqlite3 with the .backup command for a consistent online snapshot, gzip the result, and push off-site to object storage. Schedule from cron after low-traffic hours, often 02:00 NPT in Nepal. Keep seven daily and four weekly copies minimum. A Rs 500/month (~USD 3.70) object storage bucket is cheap insurance. Test a restore quarterly to confirm backups are actually usable, not just sitting in a bucket.

SQLite allows one writer at a time per database file. Readers proceed in WAL mode, but writers queue. That works fine for a contact form but not for an order pipeline during a sale. Watch for database is locked errors even with busy_timeout, queue workers and HTTP requests fighting over long transactions, and backup windows exceeding available disk I/O during business hours. Keep production files under a few gigabytes for sane backup times despite terabyte theoretical limits on paper.

Migrate when you see repeated lock timeouts, need remote database access from multiple apps, run heavy write queues, require replication, or need point-in-time recovery. Other signals include growing file size, need for a separate reporting replica, or BI tool connections. Plan the move before peak season such as Dashain traffic spikes if you serve Nepal retail audiences. Most client portals I build use MySQL early because document uploads, payments, and concurrent staff logins arrive quickly. Emergency migration under load is far worse than planning ahead.

The sqlite3 binary lets you inspect schema and row counts without booting the full Laravel app. Use .tables to list tables, SELECT COUNT(*) for quick checks, and -header -column for readable output when SSHing into a client VPS. Wrap bulk imports in BEGIN and COMMIT transactions because one row per commit on a thousand rows is painfully slow on HDD VPS plans common at Rs 800–1,500/month (~USD 6–11). Export CSV with -header -csv for spreadsheet reporting when you outgrow manual review.

Both run fine on a five to fifteen dollar VPS. SQLite has no daemon overhead and excels on shared VPS hosting with simple file backups, but only allows one concurrent writer. MySQL 8.4 LTS adds a mysqld daemon and memory buffer pool but supports many concurrent writers with row-level locking, standard TCP remote access, and pairs better with Laravel Horizon and Redis queues. Pick SQLite when you want the smallest ops surface. Pick MySQL when staff log in together, payments land, or you already plan e-commerce features.

Skipping MySQL saves roughly 200 to 400 MB RAM on a 1 GB VPS, which matters at Rs 1,200/month hosting tiers common in Nepal. You also avoid mysqld monitoring and tuning overhead entirely. Nightly gzip backups to object storage add about Rs 500/month (~USD 3.70). The savings become meaningless once lost orders from lock contention cost more than the server bill. Evaluate workload fit and business risk over raw monthly hosting price when making the decision.

SQLite lacks SHOW PROCESSLIST, so monitor file size, lock errors in application logs, and page latency instead. Run PRAGMA integrity_check weekly via sqlite3 or a custom artisan command. Pair with application tests on every deploy. Document migration triggers in the README so the next developer knows when to leave SQLite. Include SQLite checks in support and maintenance retainers the same way you would run mysqlcheck on larger stacks. WAL files grow until checkpointed, and file permissions drift after manual edits.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: