Next.js, SQLite, and Docker, the technical stack behind this blog
Every technical decision has a reason behind it. We chose Next.js over Astro, SQLite over PostgreSQL, and Docker with Dokploy over Vercel. Here’s why, and what we learned along the way.

When you decide to build something from scratch, the first decisions are the ones with the biggest impact. The framework, the database, the ORM, and the deployment strategy define the ceiling of what you can do and the floor of what you're going to suffer through.
In our case, the stack ended up like this. Next.js as the framework, SQLite as the database, Drizzle as the ORM, TypeScript everywhere, and Docker with Dokploy for deployment. Every choice has a story behind it.
Why Next.js and not Astro, Remix, or SvelteKit
Next.js isn't perfect. Its complexity curve ramps up fast when you mix Server Components, Server Actions, and dynamic routes. But it had something no other framework gave us in the same package. A mature router with App Router, native support for ISR (Incremental Static Regeneration), built-in API routes, and a huge ecosystem of compatible libraries.
Astro was tempting because of its static performance, but our blog needs a full admin panel with authentication, a WYSIWYG editor, file uploads, and CRUD operations. That's a web application, not a static site. Astro can do it with React islands, but at that point you're fighting the framework instead of using it.
Remix and SvelteKit are excellent, but the community and component ecosystem (shadcn/ui, Tiptap, Shiki) are much more mature in React and Next.js. For a project where we want to add features every week without having to write everything from scratch, that matters a lot.
SQLite, the database that doesn't need a server
This was probably the most counterintuitive decision. "SQLite for production? Seriously?" Yes, seriously. And it works better than most people expect.
A personal blog isn't an application with thousands of concurrent writes. It's an application where one user (the admin) writes content and thousands of readers consume it. That pattern of heavy reads with occasional writes is exactly where SQLite shines, thanks to WAL mode (Write-Ahead Logging).
The advantages are huge in terms of operational simplicity.
- Zero extra infrastructure. There's no PostgreSQL server to maintain, monitor, update, or back up separately. The database is a file.
- Trivial backups. Copying the file is a full backup. We do it automatically every night with a cron job that runs a WAL checkpoint and copies the file.
- Ridiculously fast reads. Queries don't travel over the network. They go straight to disk (or memory if the OS caches the file). Queries that take 5ms in PostgreSQL take microseconds here.
- Total portability. To develop locally, you clone the repo, run npm install, and you already have the database. No docker compose up for PostgreSQL, no credential setup, no waiting for the container to start.
The configuration pragmas we use tune SQLite for our use case. WAL for concurrency, a 30-second busy_timeout to avoid lockups, a 20 MB in-memory cache, and 64 MB of memory-mapped I/O so frequent reads don't hit disk.
Drizzle ORM, SQL with types
We tried Prisma first. It's the most popular ORM in the TypeScript ecosystem, and for good reason. But Prisma generates its own client, needs a generate step after every schema change, and its SQLite support has limitations that gave us trouble with migrations.
Drizzle is different. The schema is defined in plain TypeScript, queries are type-safe down to the column level, and migrations are generated by comparing the current schema with the previous one. No intermediate binaries, no code generation. Just TypeScript. We talk about this in more detail in migrations with Drizzle ORM.
The flow is simple. You edit the schema in drizzle/schema.ts, run pnpm db:generate to create the migration SQL, review the generated SQL, and apply it with pnpm db:migrate. If something doesn't line up, the SQL is right there for you to read and understand exactly what's going to happen.
Docker and Dokploy, deployment without vendor lock-in
Vercel is the obvious option for Next.js. It's from the same team, deployment is instant, and the free tier is generous. But Vercel has limitations that made us uncomfortable.
The main one is that Vercel runs each route as a serverless function. That means SQLite doesn't work because there's no persistent filesystem between invocations. We'd need to move to PostgreSQL with Neon or Turso, adding complexity and network latency that we'd deliberately avoided.
The alternative was self-hosting with Docker on a VPS. The Dockerfile uses a multi-stage build with Alpine Linux that produces a final image of just a few megabytes. The entrypoint automatically runs the WAL checkpoint, pending migrations, the admin user seed, and starts the server. From a security perspective, we go deeper into this in supply chain security in Docker.
For orchestration we use Dokploy, an open source PaaS that installs on any VPS and manages the application lifecycle. Every push to main triggers an automatic build and deployment. Traefik handles the reverse proxy and SSL certificates. The result is an experience similar to Vercel, but on our own infrastructure, without artificial limits and with a predictable cost. If you want to go deeper, we cover it in detail in infrastructure with Dokploy.
TypeScript everywhere
The blog is TypeScript from top to bottom. From the database schema to the React components, including Server Actions and API routes. That isn't accidental.
When the Drizzle schema defines that a field is text("status").$type<"draft" | "published" | "scheduled">(), TypeScript makes sure you never pass an invalid value anywhere in the code. If you rename a field, the compiler tells you every place that needs updating. That safety net is invaluable when you're moving fast and adding features every week.
What we learned
After months of using this stack in production, the lessons are pretty clear.
- SQLite is viable in production for applications with a read-heavy pattern. You don't need PostgreSQL for everything.
- You pay for vendor lock-in later. Self-hosting takes more work up front, but it gives you freedom that's priceless when you need to do something the platform doesn't allow.
- Drizzle is an underrated gem. The development experience with type-safety all the way down to the query level is better than any other ORM we've tried.
- Docker simplifies more than it complicates. Once you have the Dockerfile, deployment is reproducible, portable, and predictable.
- End-to-end TypeScript isn't a luxury. It's the difference between "it works on my machine" and "it works".
"The best stack isn't the most popular one. It's the one that lets you move fast without tripping over your own decisions."
Another entry in the Building this blog series. You came from Why I built my own blog engine and the next one is Security, SEO, and performance.

Jose, author of the blog
QA Engineer. I write out loud about automation, AI and software architecture. If something here helped you, write to me and tell me about it.
Leave the first comment
What did you think? What would you add? Every comment sharpens the next post.
If you liked this

Seguridad, SEO y rendimiento en un blog autoalojado
Autoalojar un blog significa que la seguridad, el SEO y el rendimiento son tu responsabilidad. Cabeceras CSP, rate limiting, imágenes OG generadas, ISR y las lecciones que aprendimos configurándolo todo.

Por qué construí mi propio motor de blog en Next.js (en lugar de WordPress o Ghost)
Teníamos WordPress, Ghost y decenas de plataformas a un clic de distancia. Pero ninguna nos daba lo que necesitábamos sin compromisos. Esta es la historia de por qué acabamos escribiendo nuestro propio motor de blog.

Cómo gestionamos las migraciones de base de datos con Drizzle ORM
El flujo completo de migraciones en proyectos reales con Drizzle ORM, desde el esquema en TypeScript hasta producción. Con comparativa frente a Prisma y lecciones aprendidas.