<?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>DEV Community: Clara Dupont</title>
    <description>The latest articles on DEV Community by Clara Dupont (@clara_dupont).</description>
    <link>https://dev.to/clara_dupont</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4190064%2F1bd5ec93-4fa5-403d-81ba-45c80958914b.png</url>
      <title>DEV Community: Clara Dupont</title>
      <link>https://dev.to/clara_dupont</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/clara_dupont"/>
    <language>en</language>
    <item>
      <title>Self-Hosting prompts.chat: A Privacy-First Prompt Library Behind Traefik</title>
      <dc:creator>Clara Dupont</dc:creator>
      <pubDate>Sun, 18 Oct 2026 08:53:12 +0000</pubDate>
      <link>https://dev.to/clara_dupont/self-hosting-promptschat-a-privacy-first-prompt-library-behind-traefik-3c27</link>
      <guid>https://dev.to/clara_dupont/self-hosting-promptschat-a-privacy-first-prompt-library-behind-traefik-3c27</guid>
      <description>&lt;h2&gt;
  
  
  Why prompts.chat Belongs in Your Homelab
&lt;/h2&gt;

&lt;p&gt;When testing &lt;a href="https://github.com/f/prompts.chat" rel="noopener noreferrer"&gt;f/prompts.chat&lt;/a&gt; — the open-source prompt library formerly known as Awesome ChatGPT Prompts — I needed a self-hosted instance that wouldn't leak internal prompts to third-party SaaS. The project ships as a static site, but real production deployments need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reverse proxy with automatic HTTPS&lt;/li&gt;
&lt;li&gt;Zero-trust network access (no public exposure)&lt;/li&gt;
&lt;li&gt;Persistent storage for custom prompts&lt;/li&gt;
&lt;li&gt;Local authentication layer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here's the battle-tested stack I landed on after two failed attempts with bare Nginx and accidental public exposure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture: Traefik + Tailscale + Docker
&lt;/h2&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;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&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;prompts-chat&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;ghcr.io/f/prompts.chat:latest&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;prompts-chat&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./data:/app/data&lt;/span&gt;
    &lt;span class="na"&gt;labels&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;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.prompts.rule=Host(`prompts.homelab.local`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.prompts.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.prompts.tls=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.services.prompts.loadbalancer.server.port=3000"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;homelab&lt;/span&gt;

  &lt;span class="na"&gt;traefik&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;traefik:v2.10&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;traefik&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
    &lt;span class="na"&gt;command&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;--api.insecure=false"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.websecure.address=:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.tailscale.acme.tlschallenge=true"&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;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./traefik-certs:/letsencrypt&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;homelab&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;homelab&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;bridge&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Zero-Trust Routing with Tailscale ACL
&lt;/h2&gt;

&lt;p&gt;Instead of exposing port 443 to the internet, bind Traefik to your Tailscale interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;tailscale-acl.json&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"acls"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"accept"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"src"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"group:engineering"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"dst"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"tag:homelab:443"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tagOwners"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"tag:homelab"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"admin@yourorg.com"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apply the policy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tailscale up &lt;span class="nt"&gt;--advertise-tags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;tag:homelab
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw allow &lt;span class="k"&gt;in &lt;/span&gt;on tailscale0 to any port 443
&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw deny 443  &lt;span class="c"&gt;# block public interface&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Deployment &amp;amp; Verification
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Start stack&lt;/span&gt;
docker-compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Verify Traefik routes&lt;/span&gt;
docker logs traefik | &lt;span class="nb"&gt;grep &lt;/span&gt;prompts

&lt;span class="c"&gt;# Test internal access&lt;/span&gt;
curl &lt;span class="nt"&gt;-k&lt;/span&gt; https://prompts.homelab.local

&lt;span class="c"&gt;# Confirm external block&lt;/span&gt;
curl &lt;span class="nt"&gt;-I&lt;/span&gt; https://&amp;lt;public-ip&amp;gt;:443  &lt;span class="c"&gt;# should timeout&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  War Story: SQLite Corruption Under Load
&lt;/h2&gt;

&lt;p&gt;During initial testing with three concurrent users editing prompts, the app crashed with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SQLite database is locked (5)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Root cause: prompts.chat uses a single SQLite file without WAL mode. Fix:&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;# Enable Write-Ahead Logging&lt;/span&gt;
sqlite3 data/prompts.db &lt;span class="s2"&gt;"PRAGMA journal_mode=WAL;"&lt;/span&gt;

&lt;span class="c"&gt;# Verify&lt;/span&gt;
sqlite3 data/prompts.db &lt;span class="s2"&gt;"PRAGMA journal_mode;"&lt;/span&gt;
&lt;span class="c"&gt;# Should return: wal&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After enabling WAL, sustained 10+ concurrent write operations with zero locks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accidental Public Exposure (and the Fix)
&lt;/h2&gt;

&lt;p&gt;First deployment mistake: I exposed Traefik dashboard on &lt;code&gt;:8080&lt;/code&gt; without authentication. Shodan indexed it within 4 hours.&lt;/p&gt;

&lt;p&gt;The hard lesson: &lt;strong&gt;every listening port needs explicit access control&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Fixed config:&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;# Traefik dynamic config (traefik-config.yml)&lt;/span&gt;
&lt;span class="na"&gt;http&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;middlewares&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;basic-auth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;basicAuth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;users&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;admin:$apr1$H6uskkkW$IgXLP6ewTrSuBkTrqE8wj/"&lt;/span&gt;  &lt;span class="c1"&gt;# htpasswd generated&lt;/span&gt;

  &lt;span class="na"&gt;routers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;traefik-dashboard&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;rule&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Host(`traefik.homelab.local`)"&lt;/span&gt;
      &lt;span class="na"&gt;service&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;api@internal&lt;/span&gt;
      &lt;span class="na"&gt;middlewares&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;basic-auth&lt;/span&gt;
      &lt;span class="na"&gt;entryPoints&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;websecure&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mount this config:&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;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./traefik-config.yml:/etc/traefik/dynamic/config.yml:ro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Performance Notes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Cold start: ~2.3s (Node.js SSR)&lt;/li&gt;
&lt;li&gt;Warm response: &amp;lt;80ms (cached static assets)&lt;/li&gt;
&lt;li&gt;Memory footprint: 140MB (prompts.chat) + 95MB (Traefik)&lt;/li&gt;
&lt;li&gt;Disk I/O: negligible after WAL mode&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When NOT to Self-Host This
&lt;/h2&gt;

&lt;p&gt;Skip this setup if:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Your team is distributed without VPN — the Tailscale ACL model assumes a private network backbone&lt;/li&gt;
&lt;li&gt;You need multi-region replication — SQLite won't scale horizontally&lt;/li&gt;
&lt;li&gt;Compliance requires audit trails — you'll need to add event logging middleware&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Next Steps
&lt;/h2&gt;

&lt;p&gt;For production-grade deployments, add:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Automated SQLite backups (&lt;code&gt;litestream&lt;/code&gt; to S3)&lt;/li&gt;
&lt;li&gt;Prometheus metrics export from Traefik&lt;/li&gt;
&lt;li&gt;Fail2ban rules for repeated auth failures&lt;/li&gt;
&lt;li&gt;Health check endpoint monitoring&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The complete stack runs on a $5/month VPS with 1GB RAM. Total setup time: 18 minutes after sorting out the SQLite locking issue.&lt;/p&gt;

&lt;p&gt;No affiliate links. No sponsored tools. Just a working reference architecture for keeping your prompt library private.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>linux</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
