Case study · Engineering
Building this blog
Rails 8, SQLite in production, deployed via Kamal, sharing one small VPS with a live-money trading bot.
安Ho Chi Minh City ·
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
/uphealthcheck. - Daily SQLite hot-backup runs inside Puma via an
on_bootedhook, 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 →