Bui Duc An
senior backend engineer · Ho Chi Minh City

Case study · Engineering

Building this blog

Rails 8, SQLite in production, deployed via Kamal, sharing one small VPS with a live-money trading bot.

Problem

I wanted somewhere to write personal essays in Vietnamese and technical notes in English that also demonstrates the craft I bring to fintech work. Medium, Substack, and Facebook capture the content and change the reader experience at whim. Static site generators are fine until you want draft workflows, backlinks, revision history, or a real admin surface.

I had one hard constraint: I already ran a small VPS for crypto_watchtower, a live-money trading bot. The blog had to fit on the same box without ever stealing resources from it.

Constraints

  • Single operator. Maintenance had to stay light for years.
  • Shared host with an unrelated live-money service. The blog must always lose under contention.
  • No paid infrastructure beyond one small VPS (~US$5/mo) and the domain.
  • Vietnamese content: proper Unicode support end-to-end for input, slugs, search, meta tags.
  • Public surface, so security matters. CSP, HSTS, admin gated, brakeman clean.
  • Rails and Ruby are my home stack. Choose tools that lean on eleven years of experience rather than novelty.

Approach

Rails 8 · SQLite in production · Kamal + Docker · Propshaft · Tailwind · Turbo/Stimulus · self-hosted WOFF2 fonts · Cloudflare in front for DNS and edge cache.

Three decisions most engineers question at first glance are worth calling out.

1. SQLite in production

Rails 8 supports it. For a personal blog with one writer and many readers, SQLite's single-writer serialization is a non-issue. The wins are large: one file to back up (cp storage/production.sqlite3); no separate database process; no connection pool tuning; the same file opens in a GUI on a copy for casual inspection. Solid Cache, Solid Queue, and Solid Cable use their own SQLite files on the same persistent Docker volume, so the whole stack lives on one mount.

2. Shared VPS with the trading bot

Kamal cgroup budgets make co-tenancy safe. From config/deploy.yml:

  • memory: 512m: a hard cap on the blog's RSS.
  • oom-score-adj: 500: when the kernel must kill something, it must pick the blog first.
  • cpu-shares: 256: the blog gets 1/4 CPU weight relative to the default 1024. The bot always wins under contention.

The trading bot's work is time-critical: order reconciliation, exit management, position tracking. A page rendering two hundred milliseconds slower harms nobody; a bot missing an exit fill loses money.

3. Cookie-free public HTML

Rails writes a session cookie on every request that touches session storage, and every cookie forces Cloudflare to bypass edge cache. Moving csrf_meta_tags out of the layout and into a per-page content_for :head, emitted only by pages that render a form, means public pages have no Set-Cookie header at all. Combined with expires_in 5.minutes, public: true on the read-only controllers, the responses are now cacheable by Cloudflare. As of this writing the edge still reports cf-cache-status: DYNAMIC for HTML. Cloudflare doesn't cache HTML by default, and the Cache Rule that would turn it on hasn't been enabled yet. The plumbing is done; the switch isn't flipped. See trade-offs.

What shipped

Live at ducan.today. Bootstrapped May 2026; canonical host moved from a subdomain to ducan.today in September 2026 with 301 redirects preserving inbound links.

  • Bilingual: Vietnamese content plus this English portfolio surface, hreflang-clustered so search engines treat both as language variants of the same identity.
  • Admin surface with a Trix editor, autosave drafts, revision history, signed-token preview links, and [[wikilinks]] that render as backlinks in-page.
  • Vietnamese slug transliteration that handles NFD-decomposed precomposed vowels plus đơưăâêô fold, with FriendlyId history preserving old slugs so they 301 to the canonical URL.
  • CI: brakeman, bundler-audit, importmap-audit, rubocop, RSpec. 123 examples pass on every push.
  • Kamal deploy in around eighty seconds, zero-downtime, with a Rails /up healthcheck.
  • Daily SQLite hot-backup runs inside Puma via an on_booted hook, with no separate cron container.

Trade-offs, and what I would change

SQLite scales this site to five figures of visitors before I would reconsider. If the workload became write-heavy (comments, submissions, a search index that rebuilds), I would revisit. For read-heavy essay traffic, SQLite is not the constraint.

CSP still allows style-src 'unsafe-inline'. Roughly one hundred inline style="…" attributes survive from prototype speed. Moving each into a class is a scheduled refactor. It's defense-in-depth I want, not a live exploit path.

No search yet. Under twenty posts, the homepage archive is browsable. SQLite FTS5 is the plan once the archive stops fitting on one screen.

Edge caching isn't switched on yet. The HTML is cookie-free and carries public, max-age, but Cloudflare only caches HTML when a Cache Rule says so, and I haven't enabled one. Until then every page view still hits the origin. Cheap to fix; deliberately measured before claimed.

Analytics deferred. Cloudflare Web Analytics is on the shortlist. I want to see what readers actually engage with before I invest more in this English surface. Data over intuition.

ActionText's CSS ships to public. The Trix editor's stylesheet is bundled with the public .trix-content rules by Propshaft's :app shortcut. Splitting would save about 3.5 KB gz per public page but requires vendoring a gem file. Not worth the maintenance risk for a marginal payoff.

Numbers

Measured against production, September 2026:

First-visit wire (Vietnamese article) ~130 KB gzipped (HTML + CSS + JS + preloaded fonts)
Homepage HTML 17.8 KB raw · 4.9 KB gz (72% reduction)
Origin Rails response 14–84 ms across all sampled URLs
TTFB via Cloudflare, origin-served (HTML not yet edge-cached) 0.2–0.6 s from Europe; 0.4 s median on a two-hit sample
Full test suite 123 / 123 pass in 1.5 s
Known CVEs (brakeman + bundler-audit) 0
Kamal deploy ~80 s, zero downtime
Total infrastructure cost one small VPS · one .today domain

Code

Public repository: github.com/doicat/an_blog.

Working on something similar?

I'm open to remote contractor or EOR engagement from Ho Chi Minh City. Get in touch →