<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Nginx – Serverküche</title><link>https://serverkueche.de/en/tags/nginx/</link><description>Nginx – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>en-US</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Thu, 27 Aug 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/nginx/index.xml" rel="self" type="application/rss+xml"/><item><title>Host Your Own Website with Hugo – Like Serverküche Itself</title><link>https://serverkueche.de/en/tutorials/host-your-website-with-hugo/</link><pubDate>Thu, 27 Aug 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/host-your-website-with-hugo/</guid><description>Build a static website with Hugo and serve it from a Docker container behind Traefik – fast, secure, no database. Exactly the setup this site runs on.</description><content:encoded><![CDATA[<p>The page you&rsquo;re reading right now runs on exactly the setup from this tutorial: Hugo builds static HTML from Markdown files, and a tiny nginx container serves it behind Traefik. No WordPress, no database, no PHP security updates – and still a fully-featured website. This is maximum dogfooding: you&rsquo;re reading a recipe on its own result.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>Your own website with the static site generator <strong>Hugo</strong> (v0.164.0), built in a multi-stage Docker image and served by <strong>nginx 1.31</strong> behind <a href="/en/tutorials/traefik-reverse-proxy/">Traefik</a> with automatic HTTPS. By the end you&rsquo;ll have a blog-capable site under your domain, generated from simple Markdown files – and you&rsquo;ll know how to publish new posts with a single rebuild. For design we use the popular <strong>PaperMod</strong> theme; the principle applies to any Hugo theme.</p>
<p>The big advantage over a classic CMS like WordPress: there&rsquo;s no attack surface at runtime. What&rsquo;s served is plain HTML, and the container contains no interpreter and no database that could be compromised.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A server with <a href="/en/tutorials/traefik-reverse-proxy/">Traefik running as a reverse proxy</a> (provides the HTTPS certificate)</li>
<li><a href="/en/tutorials/install-docker/">Docker &amp; Docker Compose</a> installed</li>
<li>A <a href="/en/tutorials/connect-domain-to-server/">domain pointing at the server</a> – replace <code>YOUR_DOMAIN</code> below</li>
<li><code>git</code> on the server (<code>apt install git</code>) to fetch the theme</li>
</ul>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20"
     data-track-content data-content-name="Affiliate-Box · /en/tutorials/host-your-website-with-hugo/" data-content-piece="VPS 1000 G12">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Recommendation</span>
    <span title="Links marked with * are affiliate links.">Ad</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 1000 G12</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCores · 8 GB RAM · 256 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €10.36/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">A static site is frugal – the smallest VPS is plenty.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-1000-g12-iv-12m?ref=44083" rel="sponsored noopener" target="_blank"
   data-track-event="Affiliate|netcup: Affiliate-Box|VPS 1000 G12 · {page}"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Go to netcup →
</a>

  </div><div class="px-4 pb-4"><p class="not-prose my-3 flex flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200">
  <span>💶 <strong>5 € voucher</strong> for new netcup customers:</span><code data-track-voucher="36nc17844976032"
        class="rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</code>
  <span class="text-xs text-slate-500 dark:text-slate-400">(new customers only, no domains)</span>
</p></div>
</div>

<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-create-the-hugo-project">Step 1: Create the Hugo project</h3>
<p>Create the project folder. Everything that makes up the site lives in this directory – including the Docker files:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">mkdir -p /opt/hugo-demo/content/posts <span class="o">&amp;&amp;</span> <span class="nb">cd</span> /opt/hugo-demo</span></span></code></pre></div>
</div>
<p>Get the <strong>PaperMod</strong> theme. We clone it as a regular folder (not a Git submodule) so it&rsquo;s guaranteed to be in the Docker build context:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">git clone --depth<span class="o">=</span><span class="m">1</span> https://github.com/adityatelange/hugo-PaperMod.git themes/PaperMod
</span></span><span class="line"><span class="cl">rm -rf themes/PaperMod/.git</span></span></code></pre></div>
</div>
<p><code>--depth=1</code> fetches only the current state (no history), and deleting <code>.git</code> afterwards makes the theme a fixed part of your project.</p>
<h3 id="step-2-configure-hugo">Step 2: Configure Hugo</h3>
<p>The central config is <code>hugo.toml</code>. Replace <code>YOUR_DOMAIN</code> with your real domain – Hugo builds absolute links on top of it:</p>
<div class="sk-code">
  <span class="sk-code-head">TOML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="nx">baseURL</span> <span class="p">=</span> <span class="s2">&#34;https://YOUR_DOMAIN/&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">languageCode</span> <span class="p">=</span> <span class="s2">&#34;en-us&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">title</span> <span class="p">=</span> <span class="s2">&#34;My Serverküche Site&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">theme</span> <span class="p">=</span> <span class="s2">&#34;PaperMod&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">params</span><span class="p">]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">description</span> <span class="p">=</span> <span class="s2">&#34;A test blog, hosted with Hugo and Docker.&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[[</span><span class="nx">menu</span><span class="p">.</span><span class="nx">main</span><span class="p">]]</span>
</span></span><span class="line"><span class="cl">  <span class="nx">name</span> <span class="p">=</span> <span class="s2">&#34;Posts&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">url</span> <span class="p">=</span> <span class="s2">&#34;/posts/&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nx">weight</span> <span class="p">=</span> <span class="mi">1</span></span></span></code></pre></div>
</div>
<p><code>baseURL</code> is crucial: if it holds the wrong domain, all internal links and assets point nowhere. The menu links to the post overview that Hugo generates automatically from the <code>content/posts/</code> folder.</p>
<h3 id="step-3-write-content-as-markdown">Step 3: Write content as Markdown</h3>
<p>Every page is a Markdown file with a small header (front matter). The homepage:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; content/_index.md <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">title: &#34;Welcome&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">This site is generated by **Hugo** and served as static HTML by nginx –
</span></span></span><span class="line"><span class="cl"><span class="s">just like Serverküche itself.
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span></span></span></code></pre></div>
</div>
<p>And a first blog post under <code>content/posts/</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; content/posts/first-post.md <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">title: &#34;My First Post&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">date: 2026-07-20
</span></span></span><span class="line"><span class="cl"><span class="s">tags: [&#34;hugo&#34;, &#34;docker&#34;]
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">Hello world! This site runs in a Docker container behind Traefik with
</span></span></span><span class="line"><span class="cl"><span class="s">automatic HTTPS.
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span></span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Future-dated posts aren&#39;t built
  </p>
  <div class="prose-kitchen text-sm">By default Hugo skips content with a <code>date</code> in the future. If you accidentally set a later date, the post can&rsquo;t be found online (404) even though the file exists. For scheduled publishing you build deliberately with <code>hugo --buildFuture</code> – otherwise: no future dates.</div>
</div>
<h3 id="step-4-the-multi-stage-dockerfile">Step 4: The multi-stage Dockerfile</h3>
<p>Now the core. A <strong>multi-stage build</strong> separates building from serving: the first stage contains Hugo and Node, builds the site and is then thrown away. The second stage is a tiny nginx image that contains only the finished HTML – no Hugo, no source code.</p>
<p><figure class="my-6"><img src="build-schema.svg" alt="Diagram of the multi-stage Docker build: source → Hugo build stage → nginx serve stage → browser" title="Multi-stage build: stage 1 builds with Hugo and is discarded, stage 2 serves only the finished HTML with nginx" loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Multi-stage build: stage 1 builds with Hugo and is discarded, stage 2 serves only the finished HTML with nginx</figcaption></figure></p>
<div class="sk-code">
  <span class="sk-code-head">Dockerfile</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="c"># Stage 1: build the site with Hugo</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">hugomods/hugo:0.164.0</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">build</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">WORKDIR</span><span class="w"> </span><span class="s">/src</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> . .<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> hugo --gc --minify<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c"># Stage 2: serve statically with nginx</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">nginx:1.31-alpine</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> nginx.conf /etc/nginx/conf.d/default.conf<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> --from<span class="o">=</span>build /src/public /usr/share/nginx/html<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">EXPOSE</span><span class="w"> </span><span class="s">80</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">HEALTHCHECK</span> --start-period<span class="o">=</span>10s --start-interval<span class="o">=</span>2s --interval<span class="o">=</span>30s --timeout<span class="o">=</span>3s --retries<span class="o">=</span><span class="m">3</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  CMD wget -q --spider http://127.0.0.1/ <span class="o">||</span> <span class="nb">exit</span> <span class="m">1</span></span></span></code></pre></div>
</div>
<p><code>hugo --gc --minify</code> cleans up during the build (<code>--gc</code>) and compresses HTML/CSS/JS (<code>--minify</code>). The <code>COPY --from=build</code> pulls only the <code>/src/public</code> folder (the finished result) into the second stage. The <code>HEALTHCHECK</code> ensures Traefik only routes to the container once nginx actually responds.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Health check against 127.0.0.1, not localhost
  </p>
  <div class="prose-kitchen text-sm">Inside the container, <code>localhost</code> resolves to the IPv6 address <code>::1</code> first. If your nginx config only listens on IPv4 (<code>listen 80;</code>), a health check against <code>http://localhost/</code> fails with &ldquo;Connection refused&rdquo; – the container stays <code>unhealthy</code> and Traefik never routes to it. So use <code>http://127.0.0.1/</code>. (This exact bug hit me while testing this tutorial.)</div>
</div>
<h3 id="step-5-configure-nginx-properly">Step 5: Configure nginx properly</h3>
<p>Without its own config, nginx serves its bare default 404 page on typos in the URL. Hugo ships its own, design-matching <code>404.html</code> – nginx should use it:</p>
<div class="sk-code">
  <span class="sk-code-head">NGINX</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span> <span class="mi">80</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span> <span class="s">_</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">root</span> <span class="s">/usr/share/nginx/html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">index</span> <span class="s">index.html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">try_files</span> <span class="nv">$uri</span> <span class="nv">$uri/</span> <span class="p">=</span><span class="mi">404</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Serve Hugo&#39;s own 404 page instead of the nginx default
</span></span></span><span class="line"><span class="cl">    <span class="kn">error_page</span> <span class="mi">404</span> <span class="s">/404.html</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="c1"># Cache static assets for a long time (Hugo appends hashes to filenames)
</span></span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="p">~</span><span class="sr">*</span> <span class="s">\.(css|js|woff2?|png|jpg|jpeg|svg|webp|ico)</span>$ <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">expires</span> <span class="s">30d</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">add_header</span> <span class="s">Cache-Control</span> <span class="s">&#34;public,</span> <span class="s">immutable&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span></span></span></code></pre></div>
</div>
<p><code>try_files $uri $uri/ =404</code> looks for the requested file, then the directory (Hugo&rsquo;s &ldquo;pretty URLs&rdquo; like <code>/posts/first-post/</code> are folders with an <code>index.html</code>), otherwise 404. The <code>expires 30d</code> for assets is safe because Hugo assigns a new file hash on every change – the browser still loads new versions immediately.</p>
<h3 id="step-6-wire-up-compose-with-traefik">Step 6: Wire up Compose with Traefik</h3>
<p>The <code>compose.yaml</code> builds the image and attaches the container to the <code>proxy</code> network with Traefik. Replace <code>YOUR_DOMAIN</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">YAML</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">website</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</span><span class="p">:</span><span class="w"> </span><span class="l">.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">my-website:latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">restart</span><span class="p">:</span><span class="w"> </span><span class="l">unless-stopped</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">networks</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">proxy]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">labels</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.enable=true&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.website.rule=Host(`YOUR_DOMAIN`)&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.website.entrypoints=websecure&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.routers.website.tls.certresolver=le&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="s2">&#34;traefik.http.services.website.loadbalancer.server.port=80&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">networks</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">proxy</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">external</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span></span></span></code></pre></div>
</div>
<p>This is the established <a href="/en/tutorials/traefik-reverse-proxy/">Traefik recipe</a>: four labels for router, HTTPS entrypoint, certificate resolver and target port. Why no <code>ports:</code>? Because Traefik reaches the container via the <code>proxy</code> network – more on that in <a href="/en/tutorials/understanding-docker-networks/">Understanding Docker networks</a>.</p>
<h3 id="step-7-build-start-verify">Step 7: Build, start, verify</h3>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose up -d --build</span></span></code></pre></div>
</div>
<p>Docker first builds the image (the Hugo build runs in the first stage) and then starts the container. Check the status:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose ps</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">NAME                  IMAGE                SERVICE   STATUS
</span></span><span class="line"><span class="cl">hugo-demo-website-1   my-website:latest    website   Up (healthy)</span></span></code></pre></div>
</div>
<p>Wait for <code>healthy</code> (without it Traefik won&rsquo;t route). Then verify from outside that the site responds with valid HTTPS:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">curl -sI https://YOUR_DOMAIN/ <span class="p">|</span> head -1
</span></span><span class="line"><span class="cl">curl -s https://YOUR_DOMAIN/ <span class="p">|</span> grep -o <span class="s1">&#39;content=&#34;Hugo [0-9.]*&#34;&#39;</span></span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">HTTP/2 200
</span></span><span class="line"><span class="cl">content=&#34;Hugo 0.164.0&#34;</span></span></code></pre></div>
</div>
<p>The second command reads the <code>generator</code> meta tag – a nice proof that Hugo 0.164.0 really built the site. In the browser you now see your homepage:</p>
<p><figure class="my-6"><img src="/en/tutorials/host-your-website-with-hugo/hugo-home_hu_b92a5a5f0b2f30eb.webp" srcset="/en/tutorials/host-your-website-with-hugo/hugo-home_hu_653801c263c95636.webp 480w, /en/tutorials/host-your-website-with-hugo/hugo-home_hu_b92a5a5f0b2f30eb.webp 768w, /en/tutorials/host-your-website-with-hugo/hugo-home_hu_eefa3dbfce7bfbee.webp 1200w, /en/tutorials/host-your-website-with-hugo/hugo-home_hu_759d4bef68d7a0b9.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/host-your-website-with-hugo/hugo-home_hu_8be8bd040f63076.webp"
    alt="The Hugo-generated homepage with the PaperMod theme under your own HTTPS domain" title="The homepage – plain HTML, served by nginx behind Traefik"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The homepage – plain HTML, served by nginx behind Traefik</figcaption></figure></p>
<p>Clicking a post shows the rendered Markdown content with date and tags:</p>
<p><figure class="my-6"><img src="/en/tutorials/host-your-website-with-hugo/hugo-post_hu_63af8811eb1f92a.webp" srcset="/en/tutorials/host-your-website-with-hugo/hugo-post_hu_5b9b3eac840c4af1.webp 480w, /en/tutorials/host-your-website-with-hugo/hugo-post_hu_63af8811eb1f92a.webp 768w, /en/tutorials/host-your-website-with-hugo/hugo-post_hu_2e8d73175e561e96.webp 1200w, /en/tutorials/host-your-website-with-hugo/hugo-post_hu_bb41a6378cb71922.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/host-your-website-with-hugo/hugo-post_hu_6acc2a057c23e9d2.webp"
    alt="A rendered blog post with title, date, body text and tags" title="The first post – generated from a single Markdown file"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The first post – generated from a single Markdown file</figcaption></figure></p>
<p>And a wrong URL doesn&rsquo;t land on the nginx default page but on Hugo&rsquo;s own 404 – thanks to <code>error_page</code>:</p>
<p><figure class="my-6"><img src="/en/tutorials/host-your-website-with-hugo/hugo-404_hu_c705ec25b988a111.webp" srcset="/en/tutorials/host-your-website-with-hugo/hugo-404_hu_8b6272f4d294b8ae.webp 480w, /en/tutorials/host-your-website-with-hugo/hugo-404_hu_c705ec25b988a111.webp 768w, /en/tutorials/host-your-website-with-hugo/hugo-404_hu_c0dff1cf47ca8997.webp 1200w, /en/tutorials/host-your-website-with-hugo/hugo-404_hu_6bd53fba95a973df.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/host-your-website-with-hugo/hugo-404_hu_45b33ae46f06bf7e.webp"
    alt="Hugo&rsquo;s styled 404 page in the PaperMod look instead of the nginx default" title="The custom 404 page thanks to error_page in the nginx config"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">The custom 404 page thanks to error_page in the nginx config</figcaption></figure></p>
<h3 id="step-8-publishing-new-posts">Step 8: Publishing new posts</h3>
<p>Everyday use is dead simple: create a new Markdown file, rebuild:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">cat &gt; content/posts/second-post.md <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">title: &#34;Second Post&#34;
</span></span></span><span class="line"><span class="cl"><span class="s">date: 2026-07-21
</span></span></span><span class="line"><span class="cl"><span class="s">---
</span></span></span><span class="line"><span class="cl"><span class="s">Another post – online right after the rebuild.
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">docker compose up -d --build</span></span></code></pre></div>
</div>
<p>Because the build takes only seconds and everything lives in the same folder, you can comfortably put this project into a Git repository (e.g. your <a href="/en/tutorials/forgejo-git-server/">Forgejo</a>) and have the rebuild triggered automatically on every push by an <a href="/en/tutorials/forgejo-actions-runner/">Actions runner</a> – that&rsquo;s how Serverküche runs its own site.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>The container stays <code>unhealthy</code>.</strong> Almost always the IPv6 trap from step 4: the health check queries <code>localhost</code> but nginx only listens on IPv4. Switch to <code>http://127.0.0.1/</code>. Check with <code>docker inspect --format '{{.State.Health.Status}}' CONTAINER</code>.</p>
<p><strong>Traefik shows <code>404 page not found</code> (instead of your site).</strong> That&rsquo;s the Traefik 404, not the nginx one – Traefik finds no matching router. Most common causes: the container isn&rsquo;t <code>healthy</code> yet, the <code>Host()</code> rule has the wrong domain, or the <code>proxy</code> network isn&rsquo;t <code>external</code>. See the 502/404 chapter in <a href="/en/tutorials/understanding-docker-networks/">Understanding Docker networks</a>.</p>
<p><strong>All links and images are broken.</strong> The <code>baseURL</code> in <code>hugo.toml</code> doesn&rsquo;t match the real domain. Hugo bakes absolute URLs based on this value – fix it and rebuild.</p>
<p><strong>A post doesn&rsquo;t appear.</strong> Check the <code>date</code> in the front matter: if it&rsquo;s in the future, Hugo won&rsquo;t build the post (see the warning box in step 3). A <code>draft: true</code> also hides content in a normal build.</p>
<p><strong><code>theme &quot;PaperMod&quot; not found</code> during the build.</strong> The theme folder is missing from the build context – usually because it&rsquo;s a Git submodule that wasn&rsquo;t copied, or a <code>.dockerignore</code> excludes it. Store the theme as a real folder as in step 1.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>Updates are relaxed.</strong> There&rsquo;s no running software with security holes at runtime – only two pinned build blocks. Occasionally bump the Hugo tag (<code>hugomods/hugo:0.164.0</code>) and <code>nginx:1.31-alpine</code> and rebuild; otherwise your normal <a href="/en/tutorials/keep-docker-stack-updated/">update process</a> handles it. Update the theme when needed with a fresh <code>git clone</code>.</li>
<li><strong>The backup is trivial – but important.</strong> Your entire website <em>is</em> the project folder (Markdown, <code>hugo.toml</code>, theme, Docker files). It belongs in a Git repository and/or your <a href="/en/tutorials/restic-backups/">Restic backup</a>. You don&rsquo;t need to back up the generated HTML files – they&rsquo;re regenerated from the source at any time.</li>
<li><strong>No database risk.</strong> Because there&rsquo;s no database and no login, the biggest maintenance burden of classic CMSes is gone. The only &ldquo;state&rdquo; of your site is the content you write yourself – versioned in Git, it&rsquo;s both backup and change history.
</content></li>
</ul>
]]></content:encoded></item></channel></rss>