<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Vasyl</title>
    <description>The latest articles on Forem by Vasyl (@mrviduus).</description>
    <link>https://forem.com/mrviduus</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F333461%2Fea3cc6b2-e942-4848-8606-30c345279779.jpg</url>
      <title>Forem: Vasyl</title>
      <link>https://forem.com/mrviduus</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/mrviduus"/>
    <language>en</language>
    <item>
      <title>I Quit Designing Data-Intensive Applications (DDIA) Three Times. Here's What I Build on the Fourth Try.</title>
      <dc:creator>Vasyl</dc:creator>
      <pubDate>Wed, 22 Apr 2026 05:14:01 +0000</pubDate>
      <link>https://forem.com/mrviduus/i-quit-designing-data-intensive-applications-ddia-three-times-heres-what-i-build-on-the-fourth-5bom</link>
      <guid>https://forem.com/mrviduus/i-quit-designing-data-intensive-applications-ddia-three-times-heres-what-i-build-on-the-fourth-5bom</guid>
      <description>&lt;p&gt;In 2023 I bought DDIA on Kindle. Opened the replication chapter. Quit after 40 pages and didn't open it for six months.&lt;/p&gt;

&lt;p&gt;In 2024 I bought it again, because the book is clearly worth finishing. Got to page 80. Closed it.&lt;/p&gt;

&lt;p&gt;In 2025 I tried a third time with ChatGPT open in another tab to explain the hard terms. It got easier. But every lookup was the same loop — alt-tab, paste the sentence, wait, come back, find my place. After three chapters I wasn't really reading the book anymore. I was reading my own habit of switching tabs.&lt;/p&gt;

&lt;p&gt;The book still sits in my Kindle library, marked unfinished. If you have a book like that on your shelf, this post is for you. I finally figured out why I kept quitting, and built a tool that fixes it for me. Maybe it fixes it for you too.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually breaking
&lt;/h2&gt;

&lt;p&gt;When I quit for the third time, I sat down and tried to be honest about what was stopping me.&lt;/p&gt;

&lt;p&gt;It wasn't that the book was too hard. I understood most of what was on the page. The problem was the rest — the unfamiliar terms.&lt;/p&gt;

&lt;p&gt;Every unknown term forced a decision between two bad options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option one: stop and look it up.&lt;/strong&gt; Alt-tab, paste the sentence, wait, come back, find my place. Flow broken. The next paragraph is harder to hold in your head.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option two: skip it and hope context saves me.&lt;/strong&gt; Sometimes it does. But after a dozen skips in a chapter, the quality of my reading drops noticeably. And each "I'll figure it out later" turns into debt.&lt;/p&gt;

&lt;p&gt;The exhaustion wasn't coming from reading. It was coming from the constant small decisions.&lt;/p&gt;

&lt;p&gt;There was a third problem too. Even when I did look something up, a week later I'd forgotten it. ChatGPT doesn't remember you asked. Anki remembers, but making cards by hand is its own pile of friction. I was learning words in order to forget them. And reading books in order to quit them.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got wrong about AI and reading
&lt;/h2&gt;

&lt;p&gt;When ChatGPT arrived, a lot of people thought long books were dead. Why read 600 pages of DDIA when you can ask and get a summary in a minute?&lt;/p&gt;

&lt;p&gt;I believed that for about a year.&lt;/p&gt;

&lt;p&gt;Then I sat in a 2025 interview being asked about replication strategies in distributed systems, and realized I couldn't explain the difference between synchronous and asynchronous replication past surface-level buzzwords. I'd read dozens of summaries, listened to podcasts, watched YouTube breakdowns. I knew things on the surface. I didn't understand any of them deeply.&lt;/p&gt;

&lt;p&gt;For staying current, summaries are fine. For real understanding, nothing replaces sitting with a book that someone spent years structuring. Those are exactly the books I kept quitting around page 40.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I built
&lt;/h2&gt;

&lt;p&gt;In January 2026 I started building what became TextStack — a reader where I could read technical books without the tab switching.&lt;/p&gt;

&lt;p&gt;The idea is simple. Tap a word you don't know. An explanation appears inline — not a dictionary entry, but a short concept explanation from Claude that takes into account what the book is about and what the sentence is doing. For everyday words, a short translation. For technical terms like RLHF, attention mechanism, or eventual consistency — two or three sentences on what it is and why it matters, with links to related ideas and common confusions.&lt;/p&gt;

&lt;p&gt;The word goes into a personal dictionary automatically. But not the way LingQ does it, where your review queue grows to hundreds of items and you quit the app. I built a filter — only words from roughly the top 15,000 English words by frequency, or technical terms, enter spaced repetition. The rest are saved as reference. The weekly review queue is capped, so it never spirals.&lt;/p&gt;

&lt;p&gt;Over three and a half months I put together a working version on .NET 10, React, and React Native. PostgreSQL, Claude API for explanations, Edge TTS for audio, offline PWA. It ingests EPUB, PDF, and FB2. The catalog started wide, but I'm pruning it hard — I'm realizing focus matters more than I thought.&lt;/p&gt;

&lt;p&gt;It lives at &lt;a href="https://textstack.app" rel="noopener noreferrer"&gt;textstack.app&lt;/a&gt; — full pitch at the end of this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I got wrong for three months
&lt;/h2&gt;

&lt;p&gt;For the first three months I was building for an abstract "non-native English speaker who wants to read books." Nobody needs that.&lt;/p&gt;

&lt;p&gt;In April I looked at it honestly and asked who I'd actually built it for. The answer was: a developer trying to read AI engineering books. Because that's what I'd been trying to read for two years. Chip Huyen's &lt;em&gt;AI Engineering&lt;/em&gt;. &lt;em&gt;Hands-On Large Language Models&lt;/em&gt;. &lt;em&gt;Designing Machine Learning Systems&lt;/em&gt;. &lt;em&gt;Building Agentic AI Systems&lt;/em&gt;. &lt;em&gt;Prompt Engineering for LLMs&lt;/em&gt;. I bought all of them. I finished none.&lt;/p&gt;

&lt;p&gt;When I looked at other developers' reading lists online, I saw I wasn't alone. A lot of developers are trying to move into AI engineering right now. We're all reading the same books, and a lot of us aren't finishing them.&lt;/p&gt;

&lt;p&gt;This isn't a generic "non-native English" problem. It's a specific problem for a specific group going through a specific career transition.&lt;/p&gt;

&lt;p&gt;So I'm pivoting. Not "a reader for everyone." A reader for developers learning AI engineering. A narrow niche where I'm already the user.&lt;/p&gt;

&lt;h2&gt;
  
  
  The next six months
&lt;/h2&gt;

&lt;p&gt;Four things.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Rebuild the product around the AI angle.&lt;/strong&gt; Trim the catalog to 15–20 AI engineering books. Rewrite the homepage. Shift the framing from translation to explanation. Improve the prompts for technical terms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Actually start reading.&lt;/strong&gt; &lt;em&gt;Hands-On LLMs&lt;/em&gt; in May. &lt;em&gt;AI Engineering&lt;/em&gt; in June and July. &lt;em&gt;Building Agentic AI Systems&lt;/em&gt; in August. Not as a task — as something I want. I want to work as an AI engineer in two years, and the only way there is through these books. I'll read them inside TextStack, because if it doesn't work for me, it won't work for anyone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Write about the process.&lt;/strong&gt; This is the first post. If you want to follow along, the blog has RSS.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Find the first paying customer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'll say it openly:&lt;/strong&gt; if in six months there's one stranger paying for TextStack, I'll consider this project a success regardless of the other numbers. The first dollar from someone you don't know is a threshold most solo devs never cross. Crossing it is a big part of the work of leaving employment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;Live at &lt;a href="https://textstack.app" rel="noopener noreferrer"&gt;textstack.app&lt;/a&gt; — you can open a sample chapter of &lt;em&gt;Pragmatic Programmer&lt;/em&gt; or &lt;em&gt;Hands-On LLMs&lt;/em&gt; without signing up.&lt;/p&gt;

&lt;p&gt;If you're in a similar spot — non-native dev, bought the AI engineering books, didn't finish them — send me a note. Twitter: &lt;a href="https://x.com/Rexetdeus" rel="noopener noreferrer"&gt;@Rexetdeus&lt;/a&gt;. Email on the site. I'll give you early access and listen to what works and what doesn't. In exchange I need honest feedback.&lt;/p&gt;

&lt;p&gt;If it's not your thing, thanks for reading this far. If someone you know is stuck on Chapter 3 of &lt;em&gt;AI Engineering&lt;/em&gt;, maybe forward them this post.&lt;/p&gt;

&lt;h2&gt;
  
  
  P.S.
&lt;/h2&gt;

&lt;p&gt;One more thing. This problem — quitting hard books at page 40 — isn't really about English and isn't really about AI. It's that reading tools are stuck in the early 2010s while content has gotten much denser.&lt;/p&gt;

&lt;p&gt;Kindle Word Wise is from 2014, and it still shows single-word definitions that can't handle &lt;em&gt;eventual consistency&lt;/em&gt; or &lt;em&gt;attention mechanism&lt;/em&gt;. LingQ has been showing translations and adding words to SRS for close to two decades, and the core experience hasn't really changed. Readlang was a clever browser extension in 2013; development stopped when the founder went to Duolingo.&lt;/p&gt;

&lt;p&gt;Modern books need different tools. Not dictionaries — explanations. Not infinite queues — capped ones. Not one experience for everyone — context-aware understanding.&lt;/p&gt;

&lt;p&gt;That's the opening I'm walking into. I'll let you know in six months how it went.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;First post in a series about building TextStack as an AI engineering books reader. Star the repo if you want to follow along: &lt;a href="https://github.com/mrviduus/textstack" rel="noopener noreferrer"&gt;github.com/mrviduus/textstack&lt;/a&gt; · &lt;a href="https://textstack.app" rel="noopener noreferrer"&gt;textstack.app&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>machinelearning</category>
      <category>beginners</category>
    </item>
    <item>
      <title>How We Made Our React SPA Visible to Google Without Rewriting Everything</title>
      <dc:creator>Vasyl</dc:creator>
      <pubDate>Sat, 17 Jan 2026 23:31:51 +0000</pubDate>
      <link>https://forem.com/mrviduus/how-we-made-our-react-spa-visible-to-google-without-rewriting-everything-1916</link>
      <guid>https://forem.com/mrviduus/how-we-made-our-react-spa-visible-to-google-without-rewriting-everything-1916</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; We needed Google to index 500+ book pages on our SPA. Instead of migrating to Next.js or building a complex SSR solution, we added dynamic rendering with Prerender in 3 files. Here's exactly how.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: Google Can't See Your Beautiful SPA
&lt;/h2&gt;

&lt;p&gt;We built &lt;a href="https://textstack.app" rel="noopener noreferrer"&gt;TextStack&lt;/a&gt; — a free online library with a Kindle-like reader. React frontend, ASP.NET Core API, PostgreSQL. Classic stack, works great.&lt;/p&gt;

&lt;p&gt;One problem: &lt;strong&gt;Google saw nothing.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- What users see --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;As I Lay Dying by William Faulkner | TextStack&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;As I Lay Dying&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;After a woman in rural Mississippi dies...&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- 98 chapters, rich metadata, Schema.org markup --&amp;gt;&lt;/span&gt;

&lt;span class="c"&gt;&amp;lt;!-- What Googlebot saw --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Free Online Library | TextStack&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- Empty. Nothing. Void. --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We had 500+ books with beautiful SEO metadata, Schema.org structured data, Open Graph tags — all generated client-side. Googlebot executes JavaScript, but it's inconsistent and slow. Our pages weren't getting indexed.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Options We Considered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Option 1: Server-Side Rendering (Next.js/Remix)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Industry standard, great DX, built-in optimizations&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Complete frontend rewrite. Our React app was ~50 components, custom reader with offline sync, complex state management. Estimated time: 3-4 weeks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 2: Static Site Generation
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Fastest possible page loads, works everywhere&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; We tried this. Built a Next.js SSG version. It worked... until we opened it in the browser. The reader was broken. Styles were wrong. We'd essentially need to maintain two frontends.&lt;/p&gt;

&lt;h3&gt;
  
  
  Option 3: Dynamic Rendering (Prerender)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Pros:&lt;/strong&gt; Zero changes to existing React app. Add a service, configure nginx, done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cons:&lt;/strong&gt; Additional infrastructure, slight latency for first bot request.&lt;/p&gt;

&lt;p&gt;We chose &lt;strong&gt;Option 3&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Dynamic Rendering?
&lt;/h2&gt;

&lt;p&gt;Dynamic rendering means serving different content based on who's asking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Regular User (Chrome, Safari, Firefox):
  User → nginx → SPA (index.html + JS) → JS renders in browser

Search Bot (Googlebot, Bingbot):
  Bot → nginx → Prerender → Headless Chrome renders page → HTML response
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Google &lt;a href="https://developers.google.com/search/docs/crawling-indexing/javascript/dynamic-rendering" rel="noopener noreferrer"&gt;officially supports this approach&lt;/a&gt; and doesn't consider it cloaking (as long as the content is the same).&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;Here's our setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────┐
│                         nginx                                │
│  ┌─────────────────────────────────────────────────────────┐│
│  │ Check User-Agent                                        ││
│  │ Is it Googlebot/Bingbot/etc?                           ││
│  └──────────────┬────────────────────┬────────────────────┘│
│                 │ YES               │ NO                    │
│                 ▼                   ▼                       │
│  ┌──────────────────┐    ┌──────────────────┐              │
│  │ Prerender        │    │ Static Files /   │              │
│  │ (Headless Chrome)│    │ Vite Dev Server  │              │
│  └────────┬─────────┘    └──────────────────┘              │
│           │                                                 │
│           ▼                                                 │
│  ┌──────────────────┐                                      │
│  │ Fetch &amp;amp; Render   │                                      │
│  │ React App        │──────► API                           │
│  └──────────────────┘                                      │
└─────────────────────────────────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Implementation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Add Prerender Service
&lt;/h3&gt;

&lt;p&gt;We used — a lightweight Docker image with headless Chrome:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;prerender&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;tvanro/prerender-alpine:7.2.0&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;books_prerender&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MEMORY_CACHE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;      &lt;span class="c1"&gt;# Enable in-memory cache&lt;/span&gt;
      &lt;span class="na"&gt;CACHE_MAXSIZE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;500&lt;/span&gt;   &lt;span class="c1"&gt;# Cache up to 500 pages&lt;/span&gt;
      &lt;span class="na"&gt;CACHE_TTL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3600&lt;/span&gt;      &lt;span class="c1"&gt;# 1 hour cache&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3030:3000"&lt;/span&gt;
    &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;resources&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;limits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;memory&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1G&lt;/span&gt;       &lt;span class="c1"&gt;# Chrome is hungry&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Configure nginx Bot Detection
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Bot detection map&lt;/span&gt;
&lt;span class="k"&gt;map&lt;/span&gt; &lt;span class="nv"&gt;$http_user_agent&lt;/span&gt; &lt;span class="nv"&gt;$prerender_ua&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;default&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;"~*googlebot"&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;"~*bingbot"&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;"~*yandex"&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;"~*facebookexternalhit"&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;"~*twitterbot"&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;"~*linkedinbot"&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;"~*slackbot"&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;"~*whatsapp"&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;"~*applebot"&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="c1"&gt;# Add more as needed&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 3: Route Bots to Prerender
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;server&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;listen&lt;/span&gt; &lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kn"&gt;server_name&lt;/span&gt; &lt;span class="s"&gt;textstack.app&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;# Internal prerender location&lt;/span&gt;
    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/prerender-internal/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kn"&gt;internal&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_pass&lt;/span&gt; &lt;span class="s"&gt;http://prerender:3000/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_set_header&lt;/span&gt; &lt;span class="s"&gt;Host&lt;/span&gt; &lt;span class="nv"&gt;$host&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_connect_timeout&lt;/span&gt; &lt;span class="s"&gt;60s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;proxy_read_timeout&lt;/span&gt; &lt;span class="s"&gt;60s&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kn"&gt;location&lt;/span&gt; &lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="c1"&gt;# Check if bot&lt;/span&gt;
        &lt;span class="kn"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;$prerender&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$prerender_ua&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;$prerender&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;# Don't prerender static files&lt;/span&gt;
        &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="p"&gt;~&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt; &lt;span class="s"&gt;"&lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="s"&gt;.(js|css|png|jpg|svg|woff2)&lt;/span&gt;$&lt;span class="s"&gt;")&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;set&lt;/span&gt; &lt;span class="nv"&gt;$prerender&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;# Route bots to prerender&lt;/span&gt;
        &lt;span class="kn"&gt;if&lt;/span&gt; &lt;span class="s"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$prerender&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="s"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="kn"&gt;rewrite&lt;/span&gt; &lt;span class="s"&gt;^(.*)&lt;/span&gt;$ &lt;span class="n"&gt;/prerender-internal/http://&lt;/span&gt;&lt;span class="nv"&gt;$host$1&lt;/span&gt; &lt;span class="s"&gt;last&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

        &lt;span class="c1"&gt;# Normal users get SPA&lt;/span&gt;
        &lt;span class="kn"&gt;try_files&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt; &lt;span class="nv"&gt;$uri&lt;/span&gt;&lt;span class="n"&gt;/&lt;/span&gt; &lt;span class="n"&gt;/index.html&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Challenge: API Calls Inside Prerender
&lt;/h2&gt;

&lt;p&gt;Here's where it got interesting. After setting everything up, our book detail pages showed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;Error&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Failed to fetch&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; Our SPA makes API calls to fetch book data. The API URL was configured as &lt;code&gt;http://localhost:8080&lt;/code&gt;. Inside the Prerender container, &lt;code&gt;localhost&lt;/code&gt; is... the container itself. Not our API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The solution:&lt;/strong&gt; Vite's dev server proxy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// vite.config.ts&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;defineConfig&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;server&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://api:8080&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;// Docker service name&lt;/span&gt;
        &lt;span class="na"&gt;changeOrigin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;rewrite&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;api/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="c1"&gt;// Allow prerender to access via Docker network&lt;/span&gt;
    &lt;span class="na"&gt;allowedHosts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;web&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;localhost&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then we changed our API base URL:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http://localhost:8080&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// After&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;API_BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;// Relative, works everywhere&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now when Prerender's Chrome loads our SPA:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;JS executes and calls &lt;code&gt;/api/en/books/some-book&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Vite proxies to &lt;code&gt;http://api:8080/en/books/some-book&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;API returns data&lt;/li&gt;
&lt;li&gt;React renders the page&lt;/li&gt;
&lt;li&gt;Prerender captures the HTML&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Before (what Googlebot saw):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;Free Online Library | TextStack&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"root"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;title&amp;gt;&lt;/span&gt;As I Lay Dying by William Faulkner | TextStack&lt;span class="nt"&gt;&amp;lt;/title&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"description"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"After a woman in rural Mississippi dies,
her husband and five children begin an arduous journey..."&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"application/ld+json"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@context&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://schema.org&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Book&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;As I Lay Dying&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;author&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Person&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;William Faulkner&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;description&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;...&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;inLanguage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;As I Lay Dying&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;p&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"book-detail__author"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;William Faulkner&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;&lt;span class="c"&gt;&amp;lt;!-- 98 chapters with links --&amp;gt;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Performance:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;First request (cold): ~3-5 seconds (Chrome needs to render)&lt;/li&gt;
&lt;li&gt;Cached requests: ~50ms&lt;/li&gt;
&lt;li&gt;Cache hit rate: ~95% (bots recrawl the same pages)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick Test
&lt;/h2&gt;

&lt;p&gt;Want to see what Googlebot sees on your site?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Your site as a regular user&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="s2"&gt;"https://yoursite.com/page"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;title&amp;gt;"&lt;/span&gt;

&lt;span class="c"&gt;# Your site as Googlebot&lt;/span&gt;
curl &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="nt"&gt;-A&lt;/span&gt; &lt;span class="s2"&gt;"Googlebot"&lt;/span&gt; &lt;span class="s2"&gt;"https://yoursite.com/page"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;title&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the titles are different (or the second one is empty), you have an SEO problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Files Changed
&lt;/h2&gt;

&lt;p&gt;The entire implementation touched &lt;strong&gt;5 files&lt;/strong&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;File&lt;/th&gt;
&lt;th&gt;Lines&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;+20&lt;/td&gt;
&lt;td&gt;Add prerender service&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nginx.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;+85&lt;/td&gt;
&lt;td&gt;Bot detection &amp;amp; routing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;vite.config.ts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;+10&lt;/td&gt;
&lt;td&gt;API proxy for prerender&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker-compose.prod.yml&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;+18&lt;/td&gt;
&lt;td&gt;Production prerender&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nginx-prod.conf&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;+118&lt;/td&gt;
&lt;td&gt;Production bot routing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No React components changed. No business logic touched. The SPA remains exactly as it was.&lt;/p&gt;

&lt;h2&gt;
  
  
  Should You Use This?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Yes, if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You have an existing SPA that works well&lt;/li&gt;
&lt;li&gt;You need SEO but can't justify a rewrite&lt;/li&gt;
&lt;li&gt;Your content doesn't change every second&lt;/li&gt;
&lt;li&gt;You're comfortable with Docker/nginx&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;No, if:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You're starting a new project (just use Next.js)&lt;/li&gt;
&lt;li&gt;You need real-time SEO updates&lt;/li&gt;
&lt;li&gt;Your pages are highly personalized&lt;/li&gt;
&lt;li&gt;You can't add infrastructure&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;Have questions about implementing this for your SPA? Drop a comment below or open an issue on GitHub!&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;#react&lt;/code&gt; &lt;code&gt;#seo&lt;/code&gt; &lt;code&gt;#docker&lt;/code&gt; &lt;code&gt;#nginx&lt;/code&gt; &lt;code&gt;#webdev&lt;/code&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>react</category>
      <category>tutorial</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Expose Your Local Server to the Internet (Without Port Forwarding)</title>
      <dc:creator>Vasyl</dc:creator>
      <pubDate>Fri, 02 Jan 2026 17:37:18 +0000</pubDate>
      <link>https://forem.com/mrviduus/how-to-expose-your-local-server-to-the-internet-without-port-forwarding-3f3h</link>
      <guid>https://forem.com/mrviduus/how-to-expose-your-local-server-to-the-internet-without-port-forwarding-3f3h</guid>
      <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Use Cloudflare Tunnel to make your home server accessible from anywhere. Free, secure, no router configuration needed.&lt;/p&gt;




&lt;p&gt;I recently deployed a web app running on my laptop to a real domain. No cloud hosting, no VPS, no monthly bills. Just my laptop, a domain, and Cloudflare Tunnel.&lt;/p&gt;

&lt;p&gt;Here's exactly how I did it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;I wanted to host my side project on my own hardware. Sounds simple, right?&lt;/p&gt;

&lt;p&gt;But my setup had issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;No access to the main router's admin panel&lt;/li&gt;
&lt;li&gt;Dynamic IP address&lt;/li&gt;
&lt;li&gt;Port forwarding wasn't an option&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traditional solutions like opening ports 80/443 weren't going to work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Solution: Cloudflare Tunnel
&lt;/h2&gt;

&lt;p&gt;Cloudflare Tunnel (formerly Argo Tunnel) creates an &lt;strong&gt;outbound connection&lt;/strong&gt; from your server to Cloudflare's edge. No incoming ports needed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Internet → Cloudflare (SSL) → Tunnel → Your laptop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's free, handles SSL automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A domain name (any registrar works)&lt;/li&gt;
&lt;li&gt;A Cloudflare account (free tier is fine)&lt;/li&gt;
&lt;li&gt;A Linux/Mac/Windows machine running your app&lt;/li&gt;
&lt;li&gt;~10 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 1: Add Your Domain to Cloudflare
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Go to &lt;a href="https://dash.cloudflare.com" rel="noopener noreferrer"&gt;dash.cloudflare.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Add a site&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Enter your domain (e.g., &lt;code&gt;myapp.com&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Select the &lt;strong&gt;Free&lt;/strong&gt; plan&lt;/li&gt;
&lt;li&gt;Cloudflare will show you new nameservers&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 2: Update Nameservers
&lt;/h2&gt;

&lt;p&gt;Go to your domain registrar and replace the nameservers with Cloudflare's.&lt;/p&gt;

&lt;p&gt;For example, if you're using Porkbun:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to Domain Management → Your domain&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Nameservers&lt;/strong&gt; → &lt;strong&gt;Edit&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Replace with Cloudflare nameservers:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   carter.ns.cloudflare.com
   vita.ns.cloudflare.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;Save and wait 5-30 minutes for propagation&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 3: Create a Tunnel
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;In Cloudflare, go to &lt;strong&gt;Zero Trust&lt;/strong&gt; (left sidebar)&lt;/li&gt;
&lt;li&gt;Navigate to &lt;strong&gt;Networks&lt;/strong&gt; → &lt;strong&gt;Tunnels&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create a tunnel&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Name it something like &lt;code&gt;my-server&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;You'll get an installation command with a token&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 4: Install Cloudflared
&lt;/h2&gt;

&lt;p&gt;On your server, install the &lt;code&gt;cloudflared&lt;/code&gt; daemon:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ubuntu/Debian:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-L&lt;/span&gt; &lt;span class="nt"&gt;--output&lt;/span&gt; cloudflared.deb https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
&lt;span class="nb"&gt;sudo &lt;/span&gt;dpkg &lt;span class="nt"&gt;-i&lt;/span&gt; cloudflared.deb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;cloudflared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt;&lt;br&gt;
Download from &lt;a href="https://github.com/cloudflare/cloudflared/releases" rel="noopener noreferrer"&gt;GitHub releases&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 5: Connect the Tunnel
&lt;/h2&gt;

&lt;p&gt;Run the command Cloudflare gave you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;cloudflared service &lt;span class="nb"&gt;install&lt;/span&gt; &amp;lt;YOUR_TOKEN&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This installs cloudflared as a system service that starts automatically on boot.&lt;/p&gt;

&lt;p&gt;Check it's running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status cloudflared
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Add a Public Hostname
&lt;/h2&gt;

&lt;p&gt;Back in Cloudflare Dashboard:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Go to your tunnel → &lt;strong&gt;Configure&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Public Hostname&lt;/strong&gt; → &lt;strong&gt;Add a public hostname&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Fill in:

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Subdomain:&lt;/strong&gt; leave empty (or use &lt;code&gt;www&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain:&lt;/strong&gt; select your domain&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Service Type:&lt;/strong&gt; &lt;code&gt;HTTP&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;URL:&lt;/strong&gt; &lt;code&gt;localhost:80&lt;/code&gt; (or whatever port your app runs on)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Save&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you get "DNS record already exists" error, delete the existing A record for your domain in &lt;strong&gt;DNS&lt;/strong&gt; → &lt;strong&gt;Records&lt;/strong&gt; first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Test It
&lt;/h2&gt;

&lt;p&gt;Open your domain in a browser. It should load your local app with HTTPS!&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Or test from command line&lt;/span&gt;
curl https://myapp.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Running Multiple Services
&lt;/h2&gt;

&lt;p&gt;You can route different domains or paths to different local services:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;myapp.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;localhost:3000&lt;/code&gt; (frontend)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;api.myapp.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;localhost:8080&lt;/code&gt; (API)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Just add multiple public hostnames in the tunnel configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  My Setup
&lt;/h2&gt;

&lt;p&gt;Here's what I'm running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;textstack.app     → localhost:80 → nginx → React frontend
textstack.app/api → localhost:80 → nginx → Docker API (port 8080)
textstack.dev     → localhost:80 → nginx → Same app, different site
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All from a laptop sitting in my living room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Tips
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Firewall:&lt;/strong&gt; Only allow necessary ports locally
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   &lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 22/tcp   &lt;span class="c"&gt;# SSH&lt;/span&gt;
   &lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow 80/tcp   &lt;span class="c"&gt;# For tunnel (local only)&lt;/span&gt;
   &lt;span class="nb"&gt;sudo &lt;/span&gt;ufw &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Don't expose admin panels&lt;/strong&gt; to the internet. Keep them on &lt;code&gt;localhost&lt;/code&gt; only.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cloudflare adds SSL automatically&lt;/strong&gt; - no need for Let's Encrypt.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use Access Policies&lt;/strong&gt; (in Zero Trust) to require authentication for sensitive routes.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Site not loading?&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check tunnel is running&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status cloudflared

&lt;span class="c"&gt;# Check your app is running&lt;/span&gt;
curl localhost:80

&lt;span class="c"&gt;# Check DNS propagation&lt;/span&gt;
dig myapp.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;"Address Not Found" on mobile?&lt;/strong&gt;&lt;br&gt;
DNS might not have propagated to your carrier yet. Try:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Toggle airplane mode on/off&lt;/li&gt;
&lt;li&gt;Use a different DNS (1.1.1.1)&lt;/li&gt;
&lt;li&gt;Wait 15-30 minutes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;502 Bad Gateway?&lt;/strong&gt;&lt;br&gt;
Your local app isn't responding. Check it's running on the correct port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cost
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cloudflare Tunnel: &lt;strong&gt;Free&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Domain: ~$10/year&lt;/li&gt;
&lt;li&gt;Hosting: &lt;strong&gt;$0&lt;/strong&gt; (your own hardware)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When NOT to Use This
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;High-traffic production apps (your home internet has limits)&lt;/li&gt;
&lt;li&gt;Apps requiring 99.99% uptime (your laptop can crash)&lt;/li&gt;
&lt;li&gt;Sensitive data without proper security measures&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For serious production workloads, use proper cloud hosting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;Cloudflare Tunnel is perfect for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Side projects&lt;/li&gt;
&lt;li&gt;Development/staging environments&lt;/li&gt;
&lt;li&gt;Self-hosted apps&lt;/li&gt;
&lt;li&gt;Home automation dashboards&lt;/li&gt;
&lt;li&gt;Personal APIs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It took me about 15 minutes to go from "app running locally" to "app accessible worldwide with HTTPS."&lt;/p&gt;

&lt;p&gt;No cloud bills. No DevOps complexity. Just your code, running on your hardware, accessible to the world.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Have questions?&lt;/strong&gt; Drop a comment below or find me on &lt;a href="https://x.com/rexetdeus" rel="noopener noreferrer"&gt;Twitter/X&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building something cool with this setup?&lt;/strong&gt; I'd love to hear about it!&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Tags: cloudflare, self-hosting, devops, tutorial, web-development&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>networking</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Build and Push .NET 8 Apps as Docker Images (No Dockerfile)</title>
      <dc:creator>Vasyl</dc:creator>
      <pubDate>Thu, 24 Jul 2025 02:19:51 +0000</pubDate>
      <link>https://forem.com/mrviduus/build-and-push-net-8-apps-as-docker-images-no-dockerfile-mf4</link>
      <guid>https://forem.com/mrviduus/build-and-push-net-8-apps-as-docker-images-no-dockerfile-mf4</guid>
      <description>&lt;h2&gt;
  
  
  1. Why do this?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;.NET 8 can build a Docker image for you.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;One command creates and tags the image.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;You can push the image without Docker on the build server.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  2. What you need
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;.NET SDK 8.0.200+&lt;/strong&gt; — Build and publish the image.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Docker or Podman (optional)&lt;/strong&gt; — Run the image on your PC.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub or Azure DevOps&lt;/strong&gt; — Run CI/CD examples.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Build and run on your PC
&lt;/h2&gt;

&lt;p&gt;Open a terminal and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dotnet new console &lt;span class="nt"&gt;-o&lt;/span&gt; MyApp
&lt;span class="nb"&gt;cd &lt;/span&gt;MyApp
dotnet publish &lt;span class="nt"&gt;-t&lt;/span&gt;:PublishContainer &lt;span class="nt"&gt;-p&lt;/span&gt;:EnableSdkContainerSupport&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true
&lt;/span&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; myapp:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK picks the base image, tags it &lt;strong&gt;&lt;code&gt;myapp:latest&lt;/code&gt;&lt;/strong&gt;, and stores it locally.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Change image settings
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Alpine Linux&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  dotnet publish &lt;span class="nt"&gt;--os&lt;/span&gt; linux-musl &lt;span class="nt"&gt;-t&lt;/span&gt;:PublishContainer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Use Ubuntu 22.04&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  dotnet publish &lt;span class="nt"&gt;-t&lt;/span&gt;:PublishContainer &lt;span class="nt"&gt;-p&lt;/span&gt;:ContainerFamily&lt;span class="o"&gt;=&lt;/span&gt;jammy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tiny chiseled image&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  dotnet publish &lt;span class="nt"&gt;-t&lt;/span&gt;:PublishContainer &lt;span class="nt"&gt;-p&lt;/span&gt;:ContainerFamily&lt;span class="o"&gt;=&lt;/span&gt;jammy-chiseled-extra
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Set repo &amp;amp; tag&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  dotnet publish &lt;span class="nt"&gt;-t&lt;/span&gt;:PublishContainer &lt;span class="nt"&gt;-p&lt;/span&gt;:ContainerRepository&lt;span class="o"&gt;=&lt;/span&gt;ghcr.io/user/app &lt;span class="nt"&gt;-p&lt;/span&gt;:ContainerImageTags&lt;span class="o"&gt;=&lt;/span&gt;v1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Push in build&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  dotnet publish &lt;span class="nt"&gt;-t&lt;/span&gt;:PublishContainer &lt;span class="nt"&gt;-p&lt;/span&gt;:ContainerRegistry&lt;span class="o"&gt;=&lt;/span&gt;ghcr.io &lt;span class="nt"&gt;-p&lt;/span&gt;:ContainerPush&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. GitHub Actions
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;strong&gt;&lt;code&gt;.github/workflows/docker.yml&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push image&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-dotnet@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;dotnet-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;8.0.x&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;docker/login-action@v3&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;registry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ghcr.io&lt;/span&gt;
          &lt;span class="na"&gt;username&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ github.actor }}&lt;/span&gt;
          &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.GITHUB_TOKEN }}&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish image&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;dotnet publish -c Release -t:PublishContainer \&lt;/span&gt;
            &lt;span class="s"&gt;-p:ContainerRepository=ghcr.io/${{ github.repository }} \&lt;/span&gt;
            &lt;span class="s"&gt;-p:ContainerImageTags=${{ github.sha }} \&lt;/span&gt;
            &lt;span class="s"&gt;-p:ContainerPush=true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  6. Azure DevOps
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;strong&gt;&lt;code&gt;azure-pipelines.yml&lt;/code&gt;&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;trigger&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;pool&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;vmImage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;acrName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myacr.azurecr.io&lt;/span&gt;
  &lt;span class="na"&gt;imageRepo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;demo/myapp&lt;/span&gt;

&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;checkout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UseDotNet@2&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;packageType&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sdk&lt;/span&gt;
    &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;8.0.x&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;dotnet publish -c Release -t:PublishContainer \&lt;/span&gt;
      &lt;span class="s"&gt;-p:ContainerRegistry=$(acrName) \&lt;/span&gt;
      &lt;span class="s"&gt;-p:ContainerRepository=$(imageRepo) \&lt;/span&gt;
      &lt;span class="s"&gt;-p:ContainerImageTags=$(Build.BuildNumber) \&lt;/span&gt;
      &lt;span class="s"&gt;-p:ContainerPush=true&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and push&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  7. Quick fixes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Hidden folders like &lt;strong&gt;&lt;code&gt;.well-known&lt;/code&gt;&lt;/strong&gt; are missing → Rename the folder or add a custom MSBuild target.
&lt;/li&gt;
&lt;li&gt;Need &lt;strong&gt;&lt;code&gt;apt&lt;/code&gt;&lt;/strong&gt; packages → Make your own base image and set &lt;strong&gt;&lt;code&gt;ContainerBaseImage&lt;/code&gt;&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private NuGet feed&lt;/strong&gt; → Use the same NuGet auth you use in normal builds.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  8. Remember
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No Dockerfile&lt;/strong&gt; for most apps.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Images are smaller&lt;/strong&gt; and run as &lt;strong&gt;non‑root&lt;/strong&gt;.
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One command&lt;/strong&gt; can build and push.&lt;/li&gt;
&lt;/ul&gt;




</description>
    </item>
  </channel>
</rss>
