<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>pr1v8</title><link>https://pr1v8.ca/</link><description>Notes on running your own infrastructure. Written so you can do it too.</description><language>en</language><atom:link href="https://pr1v8.ca/feed.xml" rel="self" type="application/rss+xml"/><item><title>One repo for every static site you host</title><link>https://pr1v8.ca/posts/one-repo-many-static-sites/</link><guid>https://pr1v8.ca/posts/one-repo-many-static-sites/</guid><pubDate>Tue, 06 Oct 2026 12:00:00 +0000</pubDate><description>Put all your small static sites in a single repo, with one vhost each and a deploy script that cannot delete the big files it does not own.</description><content:encoded><![CDATA[<p>If you self-host for long enough you end up with a pile of small websites: a landing
page, a joke site a friend asked for in 2013, a mirror of some web toy, a redirect or
two. Each one was set up on a different afternoon, so each has its own folder, its own
nginx config written from memory, and its own idea of what a 404 looks like. None of it
is in version control.</p>
<p>This post describes the layout I moved mine into. It is nothing clever. The point is that
the layout is small enough to remember, and that redeploying is one command.</p>
<h2 id="the-layout">The layout</h2>
<div class="hl"><pre><span></span><code>sites/&lt;domain&gt;/              the web root, deployed verbatim
sites/&lt;domain&gt;/.deploy-keep  directories that live only on the server
nginx/snippets/common.conf   shared hardening, included by every vhost
nginx/snippets/php.conf      the PHP-FPM handler, for the one site that needs it
nginx/sites/&lt;domain&gt;.conf    one vhost per domain
deploy.sh                    rsync + nginx -t + reload
</code></pre></div>

<p>Everything a browser fetches is under <code>sites/</code>. Everything nginx reads is under
<code>nginx/</code>. A site that is only a redirect has a vhost and no folder.</p>
<h2 id="keep-big-files-out-of-git-without-losing-them">Keep big files out of git without losing them</h2>
<p>Two of my sites carry media that has no business in a repository: a hundred megabytes
of audio and a mirror of a web toy with its own asset packs. They stay on the server.
The folder they live in is named in the site&rsquo;s <code>.deploy-keep</code> file, one path per line:</p>
<div class="hl"><pre><span></span><code>media/
</code></pre></div>

<p>The deploy script turns each line into two rsync arguments:</p>
<div class="hl"><pre><span></span><code><span class="nv">args</span><span class="o">=(</span>-az<span class="w"> </span>--delete<span class="w"> </span>--exclude<span class="o">=</span>.deploy-keep<span class="o">)</span>
<span class="k">while</span><span class="w"> </span><span class="nv">IFS</span><span class="o">=</span><span class="w"> </span><span class="nb">read</span><span class="w"> </span>-r<span class="w"> </span>keep<span class="p">;</span><span class="w"> </span><span class="k">do</span>
<span class="w">  </span><span class="o">[</span><span class="w"> </span>-n<span class="w"> </span><span class="s2">"</span><span class="nv">$keep</span><span class="s2">"</span><span class="w"> </span><span class="o">]</span><span class="w"> </span><span class="o">||</span><span class="w"> </span><span class="k">continue</span>
<span class="w">  </span><span class="nv">args</span><span class="o">+=(</span><span class="s2">"--exclude=</span><span class="nv">$keep</span><span class="s2">"</span><span class="w"> </span><span class="s2">"--filter=protect </span><span class="nv">$keep</span><span class="s2">"</span><span class="o">)</span>
<span class="k">done</span><span class="w"> </span>&lt;<span class="w"> </span><span class="s2">"sites/</span><span class="nv">$s</span><span class="s2">/.deploy-keep"</span>
rsync<span class="w"> </span><span class="s2">"</span><span class="si">${</span><span class="nv">args</span><span class="p">[@]</span><span class="si">}</span><span class="s2">"</span><span class="w"> </span><span class="s2">"sites/</span><span class="nv">$s</span><span class="s2">/"</span><span class="w"> </span><span class="s2">"server:/srv/www/</span><span class="nv">$s</span><span class="s2">/"</span>
</code></pre></div>

<p><code>--exclude</code> stops rsync from uploading the directory. <code>--filter=protect</code> stops
<code>--delete</code> from removing it on the receiving side because it is missing from the
source. You need both. With only the exclude, the first <code>--delete</code> run wipes the media.
I tested that on a copy before trusting it, and so should you.</p>
<h2 id="one-snippet-for-the-boring-parts">One snippet for the boring parts</h2>
<p>Every vhost includes the same file:</p>
<div class="hl"><pre><span></span><code><span class="k">autoindex</span><span class="w"> </span><span class="no">off</span><span class="p">;</span>
<span class="k">server_tokens</span><span class="w"> </span><span class="no">off</span><span class="p">;</span>
<span class="k">add_header</span><span class="w"> </span><span class="s">X-Content-Type-Options</span><span class="w"> </span><span class="s">"nosniff"</span><span class="w"> </span><span class="s">always</span><span class="p">;</span>
<span class="k">add_header</span><span class="w"> </span><span class="s">X-Frame-Options</span><span class="w"> </span><span class="s">"SAMEORIGIN"</span><span class="w"> </span><span class="s">always</span><span class="p">;</span>
<span class="k">add_header</span><span class="w"> </span><span class="s">Referrer-Policy</span><span class="w"> </span><span class="s">"strict-origin-when-cross-origin"</span><span class="w"> </span><span class="s">always</span><span class="p">;</span>
<span class="k">error_page</span><span class="w"> </span><span class="mi">404</span><span class="w"> </span><span class="s">/404.html</span><span class="p">;</span>
<span class="k">location</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="s">/404.html</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kn">internal</span><span class="p">;</span><span class="w"> </span><span class="p">}</span>
<span class="k">location</span><span class="w"> </span><span class="p">~</span><span class="w"> </span><span class="sr">/\.</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kn">deny</span><span class="w"> </span><span class="s">all</span><span class="p">;</span><span class="w"> </span><span class="p">}</span>
</code></pre></div>

<p><code>autoindex off</code> is the one that mattered to me. One of my old vhosts had it on, so a
folder I had forgotten about was a browsable listing on the public internet. If you take
one thing from this post, grep your configs for <code>autoindex</code>.</p>
<p>A vhost then becomes a few lines:</p>
<div class="hl"><pre><span></span><code><span class="k">server</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kn">listen</span><span class="w"> </span><span class="mi">80</span><span class="p">;</span>
<span class="w">    </span><span class="kn">server_name</span><span class="w"> </span><span class="s">example.com</span><span class="p">;</span>
<span class="w">    </span><span class="kn">root</span><span class="w"> </span><span class="s">/srv/www/example.com</span><span class="p">;</span>
<span class="w">    </span><span class="kn">index</span><span class="w"> </span><span class="s">index.html</span><span class="p">;</span>
<span class="w">    </span><span class="kn">include</span><span class="w"> </span><span class="s">snippets/common.conf</span><span class="p">;</span>
<span class="w">    </span><span class="kn">location</span><span class="w"> </span><span class="s">/</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="kn">try_files</span><span class="w"> </span><span class="nv">$uri</span><span class="w"> </span><span class="nv">$uri/</span><span class="w"> </span><span class="nv">$uri.html</span><span class="w"> </span><span class="p">=</span><span class="mi">404</span><span class="p">;</span><span class="w"> </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p>The <code>$uri.html</code> lets <code>/about</code> serve <code>about.html</code>, so pages do not need a folder each.</p>
<h2 id="the-deploy-script">The deploy script</h2>
<p>Before anything is overwritten, the live vhost directory is copied to a timestamped
backup next to it. Then the confs are uploaded and <code>nginx -t</code> runs inside the
container. If that fails, the script stops and nothing has been reloaded; the old
confs are still in the backup. Only after the test passes does it sync the site
folders and reload.</p>
<p>That ordering is what makes it safe. A typo in a vhost cannot take the sites down,
and a bad sync is one <code>rsync</code> from the backup away from fixed.</p>
<h2 id="what-i-left-out">What I left out</h2>
<p>There is no build step for the plain sites and I intend to keep it that way. The blog
you are reading is the one exception: a hundred lines of Python turn Markdown into HTML
into the same <code>sites/</code> folder, and the deploy script runs it first. No framework, no
node_modules, nothing to upgrade.</p>]]></content:encoded></item></channel></rss>
