<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Michael Scheiwiller on Medium]]></title>
        <description><![CDATA[Stories by Michael Scheiwiller on Medium]]></description>
        <link>https://medium.com/@mischeiwiller?source=rss-2db7cef8c6a0------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*50BZbKS9iTae5lS9qdwd-Q.jpeg</url>
            <title>Stories by Michael Scheiwiller on Medium</title>
            <link>https://medium.com/@mischeiwiller?source=rss-2db7cef8c6a0------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sun, 15 Nov 2026 09:11:49 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@mischeiwiller/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[I Turned my iPhone into a Dumb Phone]]></title>
            <link>https://medium.com/@mischeiwiller/dumb-phone-0f25d89f1cba?source=rss-2db7cef8c6a0------2</link>
            <guid isPermaLink="false">https://medium.com/p/0f25d89f1cba</guid>
            <category><![CDATA[digital-minimalism]]></category>
            <category><![CDATA[productivity]]></category>
            <category><![CDATA[screentime]]></category>
            <dc:creator><![CDATA[Michael Scheiwiller]]></dc:creator>
            <pubDate>Thu, 18 Sep 2025 14:18:55 GMT</pubDate>
            <atom:updated>2025-09-18T14:25:15.257Z</atom:updated>
            <content:encoded><![CDATA[<p><em>I never thought i would be the type of person who turns their iPhone into a dumb phone (there is a reason it is a smart phone, right?). But here we go, reading about digital minimalism and how to reduce screen time. If something hookes me up, i am all in. This can be great. But it can also be really annoying.</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/828/1*q_YmH6jlYX7DRkCnLuWnOg.png" /></figure><figure><img alt="" src="https://cdn-images-1.medium.com/max/828/1*pEmo9Cxku1uSBprYxq4wpQ.png" /><figcaption>Lock- and Homescreen of my dumb iPhone</figcaption></figure><h3>My addictions</h3><p>I am an addict. I know this since my teenage years when i could not quit gaming for meals or bedtime. It also surfaced in nicotine: i first was an occasional smoker, then pretty fast a heavy smoker. I smoked ~1 package a day. When not going out. When goin out i smoked… more. I luckily changed to snus after 2 years of smoking. Because i actually liked doing sports and knew it would hurt my lung capacity. So i snused obsessivly for ~14 years. And by obsessively i mean i slept with the snus in it. And i dont start talking about alcohol (luckily this episode was shorter, still too long, but definitely shorter).</p><p><em>Obviously there are also positive aspects if you are an addictive person: The sports i picked up i did quite intense. This was the case with climbing, bouldering, mounaineering, and cycling. But this is another story.</em></p><h3>First Smartphone</h3><p>These negative experiences were among others a reason why i got my first smartphone just at 27 when i started my first office job. Since then my screen time consistently increased. In the beginning it was only browsing on the phone. Alraedy this let my toilet time explode. First i also had no social media on the phone (i deleted my fb account in 2003 for other reasons). Then i made linkedin in ~2019. Another surge in screen time. Then came corona and i tried tiktok, snapchat, twitter, and installed youtube. Another surge in screen time. I only was a consumer on all of these platforms. And just doomscrolling on these platforms without having any output just left me empty and unfullfilled. In January 2024 i then made the first posts in twitter and started to post irregularily.</p><h3>From Consumer to “Creator”</h3><p>Changing from the passive to the active part on twitter (<a href="https://x.com/mischeiwiller">@mischeiwiller</a>) changed something for me. It felt better. I got interactions. I had a goal, a measure, something to work on. Still it does obviously not helpt to reduce screen time (at least the screen time had some meaning to me). And also i lost consistency of posting and fell back to only consumerism. And this mostly on my phone.</p><h3>The Dumb Phone</h3><p>I then got across the concept of dumb phones. The first time i read it was probably from Simon <a href="https://x.com/sspaeti/status/1827761087879475553">here</a>. Crossed the topic some times again, found it interesting but never really gave it a thought that it could be for me. Too radical. But it kept popping up in my head. Then i just saw recently <a href="https://x.com/dhh/status/1967140562919977269">dhh’s post about his dumb phone</a> (although he compares it more to omarchy, in my view its the same concept). Dhh is anyway conquering my timeline lately with omarchy and i am this close to give it a try as well, but this is also another story. So this motivated me to give it a try.</p><h3>Setup</h3><p>To setup the dumb iphone i then installed the <a href="https://dumbphone.so/">dumb phone</a> app. It is quite easy to, takes about 5 minutes though. But the experience is just awesome. I love it! And when i realised it is an indieproject fom <a href="https://x.com/michael_tigas">Michael Tigas</a> i loved it even more!</p><h3>What next?</h3><p>I will keep testing the next month and see what happens to my screen time and if my enthusiasm sticks (which i really hope). I really want to reduce screen time, and this definitely feels like a good step in the right direction</p><p>[Original Text: <a href="https://www.michaelscheiwiller.com/blog/dumb-iphone">https://www.michaelscheiwiller.com/blog/dumb-iphone</a>]</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=0f25d89f1cba" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Technical Blogs]]></title>
            <link>https://medium.com/@mischeiwiller/technical-blogs-9d90e57f7200?source=rss-2db7cef8c6a0------2</link>
            <guid isPermaLink="false">https://medium.com/p/9d90e57f7200</guid>
            <category><![CDATA[technical-writing]]></category>
            <category><![CDATA[blogging]]></category>
            <dc:creator><![CDATA[Michael Scheiwiller]]></dc:creator>
            <pubDate>Mon, 04 Aug 2025 14:18:37 GMT</pubDate>
            <atom:updated>2025-08-04T14:18:37.788Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*kQ1Q1HaMfhwnHyht" /><figcaption>Photo by <a href="https://unsplash.com/@dawson2406?utm_source=medium&amp;utm_medium=referral">Stephen Dawson</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>While writing the <a href="https://medium.com/@mischeiwiller/people-to-learn-from-dcfbd46f64a1">People</a> and <a href="https://medium.com/@mischeiwiller/bookshelves-640e6f1dd2ac">Bookshelves</a> pages and browsing some of these resources i kept seeing some technical blogs the writers referred to. So i want to keep track of the ones i see here. Let me know if you have other technical blogs you like!</p><ul><li><a href="https://netflixtechblog.com/">Netflix</a></li><li><a href="https://engineering.atspotify.com/">Spotify</a></li><li><a href="https://engineering.linkedin.com/">LinkedIn</a></li><li><a href="https://aws.amazon.com/blogs/machine-learning">AWS</a></li><li><a href="https://blog.research.google/">Google</a></li><li><a href="https://ai.meta.com/blog/?page=1">Meta</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9d90e57f7200" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Bookshelves]]></title>
            <link>https://medium.com/@mischeiwiller/bookshelves-640e6f1dd2ac?source=rss-2db7cef8c6a0------2</link>
            <guid isPermaLink="false">https://medium.com/p/640e6f1dd2ac</guid>
            <category><![CDATA[books]]></category>
            <category><![CDATA[reading]]></category>
            <category><![CDATA[people]]></category>
            <dc:creator><![CDATA[Michael Scheiwiller]]></dc:creator>
            <pubDate>Mon, 04 Aug 2025 14:15:54 GMT</pubDate>
            <atom:updated>2025-08-04T14:15:54.243Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*cd95JjYSNn_D1LMO" /><figcaption>Photo by <a href="https://unsplash.com/@itfeelslikefilm?utm_source=medium&amp;utm_medium=referral">🇸🇮 Janko Ferlič</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>Im curating the <a href="https://medium.com/@mischeiwiller/people-to-learn-from-dcfbd46f64a1">People</a> page with people i like to keep on my radar for various reasons. Some of them have a separate page where they list their readings. Most do this under ‘Bookshelf’.</p><p><a href="https://x.com/mischeiwiller/status/1805216904908337404">Last year i had the topic ‘Reading’</a> and figured i liked it a lot and am sticking with it since. I keep a <a href="https://www.goodreads.com/review/list/170458419-michael?ref=nav_mybooks&amp;shelf=to-read">list of books i want to read</a> on goodreads. So i thought i can do the same with recommendations from the mentioned bookshelf concept above. This is also going to be a living list where i keep adding more. If you have some you like, i would love to know which ones.</p><p>If someon on the People site has a bookshelf type site, it is already included in the following list.</p><ul><li><a href="https://guzey.com/favorite/media/#books">https://guzey.com/favorite/media/#books</a></li><li><a href="https://karpathy.ai/books.html">https://karpathy.ai/books.html</a></li><li><a href="https://sive.rs/book">https://sive.rs/book</a></li><li><a href="https://notes.eatonphil.com/tags/books.html">https://notes.eatonphil.com/tags/books.html</a></li><li><a href="https://gwern.net/review/book">https://gwern.net/review/book</a></li><li><a href="https://minami.bearblog.dev/books/">https://minami.bearblog.dev/books/</a></li><li><a href="https://patrickcollison.com/bookshelf">https://patrickcollison.com/bookshelf</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=640e6f1dd2ac" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[People to learn from]]></title>
            <link>https://medium.com/@mischeiwiller/people-to-learn-from-dcfbd46f64a1?source=rss-2db7cef8c6a0------2</link>
            <guid isPermaLink="false">https://medium.com/p/dcfbd46f64a1</guid>
            <category><![CDATA[technology]]></category>
            <category><![CDATA[people]]></category>
            <category><![CDATA[blogging]]></category>
            <dc:creator><![CDATA[Michael Scheiwiller]]></dc:creator>
            <pubDate>Mon, 04 Aug 2025 14:11:06 GMT</pubDate>
            <atom:updated>2025-08-04T14:11:06.907Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*qnFNPFPiFtPK_YwG" /><figcaption>Photo by <a href="https://unsplash.com/@alinnnaaaa?utm_source=medium&amp;utm_medium=referral">Alina Grubnyak</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>I copied this idea from Patrick Collison who <a href="https://patrickcollison.com/people">lists people</a> who blog/write online and he likes to read from. I think this is a cool idea and will do the same here. It is going to be a living list where i add more people over time. If one of the people below has a subsite where they share their book recommendations, this site is listed on <a href="https://michaelscheiwiller.com/bookshelves">here</a>.</p><p>- <a href="https://alexdanco.com/">Alex Danco</a> (<a href="https://x.com/alex_danco">X</a>)<br>- <a href="https://guzey.com/">Alexey Guzey</a> (<a href="https://x.com/alexeyguzey">X</a>)<br>- <a href="https://karpathy.ai/">Andrej Karpathy</a> (<a href="https://karpathy.bearblog.dev/">new blog</a>], <a href="https://x.com/karpathy">X</a>)<br>- <a href="https://www.deeplearning.ai/the-batch/tag/letters/">Andrew Ng</a> (<a href="https://x.com/andrewyng">X</a>)<br>- <a href="https://balajis.com/">Balaji S. Srinivasan</a> (<a href="https://x.com/balajis">X</a>)<br>- <a href="https://www.chiphuyen.com/blog/">Chip Huyen</a> (<a href="https://x.com/chipro">X</a>)<br>- <a href="https://colah.github.io/about.html">Christopher Olah</a> (<a href="https://x.com/ch402">X</a>)<br>- <a href="https://sive.rs/">Derek Sivers</a> (<a href="https://x.com/sivers">X</a>)<br>- <a href="https://devoneriksen.substack.com/">Devon Eriksen</a> (<a href="https://x.com/devon_eriksen_">X</a>)<br>- <a href="https://notes.eatonphil.com/">Eaton Phil</a> (<a href="https://x.com/eatonphil">X</a>)<br>- <a href="https://blog.eladgil.com/">Elad Gil</a> (<a href="https://x.com/eladgil">X</a>)<br>- <a href="https://farza.substack.com/archive">Farza</a> (<a href="https://x.com/farzatv">X</a>)<br>- <a href="https://giansegato.com/">Gian Segato</a> (<a href="https://x.com/giansegato">X</a>)<br>- <a href="https://www.gregisenberg.com/blog">Greg Isenberg</a> (<a href="https://x.com/gregisenberg">X</a>)<br>- <a href="https://www.gwern.net/">Gwern Branwen</a> (<a href="https://x.com/gwern">X</a>)<br>- I<a href="https://1millionarr.substack.com/">ron Brands</a> (<a href="https://x.com/ironbrands16">X</a>)<br>- <a href="https://ricon.xyz/">José Luis Ricón Fernández de la Puente</a> (<a href="https://nintil.com/">blog</a>), (<a href="https://x.com/ArtirKel">X</a>)<br>- <a href="https://minami.bearblog.dev/blog/">Minami</a> (<a href="https://x.com/minamisatokun">X</a>)<br>- <a href="https://mitchellh.com/">Mitchell Hashimoto</a> (<a href="https://x.com/mitchellh">X</a>)<br>- <a href="https://nav.al/">Naval Ravikant</a> (<a href="https://x.com/naval">X</a>)<br>- <a href="https://patrickcollison.com/">Patrick Collison</a> (<a href="https://x.com/patrickc">X</a>)<br>- <a href="https://kalzumeus.com/">Patrick McKenzie</a> (<a href="https://bitsaboutmoney.com/archive">Bits about Money</a>), (<a href="https://x.com/patio11">X</a>)<br>- <a href="https://www.paulgraham.com/">Paul Graham</a> (<a href="https://x.com/paulg">X</a>)<br>- <a href="https://sahillavingia.com/">Sahil Lavingia</a> (<a href="https://x.com/shl">X</a>)<br>- <a href="https://sashachapin.substack.com/">Sasha Chapin</a> (<a href="https://x.com/sashachapin">X</a>)<br>- <a href="https://www.astralcodexten.com/">Scott Alexander</a> (<a href="https://x.com/slatestarcodex">X</a>)<br>- <a href="https://sebastianraschka.com/blog">Sebastian Raschka</a> (<a href="https://x.com/rasbt">X</a>)<br>- <a href="https://seths.blog/">Seth Godin</a> (<a href="https://x.com/thisissethsblog">X</a>)<br>- <a href="https://www.farnamstreetblog.com/">Shane Parrish</a> (<a href="https://x.com/shaneaparrish">X</a>)<br>- <a href="https://signull.substack.com/">Signüll</a> (<a href="https://x.com/signulll">X</a>)<br>- <a href="https://stephango.com/">Steph Ango</a> (<a href="https://x.com/kepano">X</a>)<br>- <a href="https://marginalrevolution.com/">Tyler Cowen</a>](<a href="https://x.com/tylercowen">X</a>)<br>- <a href="https://x.com/yacineMTB/articles">Yacine</a> (<a href="https://x.com/yacineMTB">X</a>)</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=dcfbd46f64a1" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[How to deploy a Node.js website with Nginx and Systemd on a VPS]]></title>
            <link>https://medium.com/@mischeiwiller/how-to-deploy-a-node-js-website-with-nginx-and-systemd-on-a-vps-4dc3fa7656bf?source=rss-2db7cef8c6a0------2</link>
            <guid isPermaLink="false">https://medium.com/p/4dc3fa7656bf</guid>
            <category><![CDATA[nodejs]]></category>
            <category><![CDATA[web-hosting]]></category>
            <category><![CDATA[vps]]></category>
            <category><![CDATA[systemd]]></category>
            <category><![CDATA[nginx]]></category>
            <dc:creator><![CDATA[Michael Scheiwiller]]></dc:creator>
            <pubDate>Thu, 31 Jul 2025 07:57:41 GMT</pubDate>
            <atom:updated>2025-07-31T07:57:41.586Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*30SSRht6L6aRnKGx9kmJqw.png" /><figcaption>Project structure</figcaption></figure><p>For some time i wanted to read the <a href="https://www.goodreads.com/book/show/29093292-the-daily-stoic">The Daily Stoic</a> but i never got to start it. So i decided to build a simple website that would serve one stoic meditation per day. With this i could get some more practice with systemd and nginx.</p><p>If you are just looking for the code, you can find it <a href="https://github.com/lappemic/366dailystoic">here</a>.</p><p><em>Disclaimer: The rest of this post is mostly copied from the [README](https://github.com/lappemic/366dailystoic/blob/main/README.md) of the project and AI generated. I just added some more details and explanations.</em></p><h3><strong>Project Overview</strong></h3><p>The Daily Stoic website is has a simple design:</p><ul><li>Purpose: Deliver one stoic meditation per day based on the current date</li><li>Design Philosophy: Clean, readable interface</li><li>Tech Stack: Minimal but robust — Node.js backend, SQLite database, vanilla HTML/CSS/JavaScript frontend</li><li>Deployment: Production-ready setup on a VPS with proper SSL and caching</li></ul><h4><strong>Tech Stack and Architecture</strong></h4><p><strong>Backend</strong></p><ul><li>Node.js + Express: Lightweight server framework</li><li>SQLite: File-based database with 366 pre-populated meditations</li><li>RESTful API: Clean endpoints for fetching meditations</li></ul><p><strong>Frontend</strong></p><ul><li>Vanilla HTML/CSS/JavaScript: No frameworks, maximum performance</li><li>Google Fonts (Crimson Text): Elegant typography for readability</li><li>Responsive Design: Works seamlessly on mobile and desktop</li></ul><p><strong>Infrastructure</strong></p><ul><li>Ubuntu VPS: Cost-effective hosting solution</li><li>Nginx: Reverse proxy with SSL termination and static file caching</li><li>Systemd: Process management and automatic restart capabilities</li><li>Let’s Encrypt: Free SSL certificates with automatic renewal</li></ul><h4><strong>Local Development Setup</strong></h4><ol><li><strong>Project Structure</strong></li></ol><pre>366dailystoic/<br>├── server.js # Express server with API routes<br>├── package.json # Dependencies and scripts<br>├── daily-stoic.db # SQLite database (586KB)<br>├── public/ # Static frontend files<br>│ ├── index.html # Main page<br>│ ├── style.css # Styling (4KB)<br>│ └── script.js # Frontend logic (3KB)<br>└── README.md # Documentation</pre><p><strong>2. Dependencies</strong></p><p>The project uses minimal dependencies:</p><pre>{<br>  &quot;dependencies&quot;: {<br>    &quot;express&quot;: &quot;⁴.18.2&quot;,<br>    &quot;sqlite3&quot;: &quot;⁵.1.6&quot;<br>  }<br>}</pre><p><strong>3. Server Configuration</strong></p><p>The Express server (<em>server.js</em>) handles:</p><ul><li>Static file serving from the <em>public/</em> directory</li><li>API endpoints for meditation data</li><li>Database connections and queries</li><li>Graceful shutdown handling</li></ul><p>Key features:</p><pre>// Serve static files<br>app.use(express.static(&#39;public&#39;))<br><br>// API endpoints<br>app.get(&#39;/api/today&#39;, (req, res) =&gt; {<br>/* Today&#39;s meditation */<br>})<br><br>app.get(&#39;/api/meditation/:month/:day&#39;, (req, res) =&gt; {<br>/* Specific date */<br>})<br><br>app.get(&#39;/api/all&#39;, (req, res) =&gt; {<br>/* All meditations for navigation */<br>})<br><br>app.get(&#39;/api/health&#39;, (req, res) =&gt; {<br>/* Health check */<br>})</pre><p><strong>4. Running Locally</strong></p><pre># Install dependencies<br>npm install<br><br># Start development server<br>npm start<br><br># Visit the application<br>open http://localhost:3000</pre><h4><strong>Production Deployment on VPS</strong></h4><p><strong>1. Server Preparation and repo clone</strong></p><p>Check out my other posts on <a href="https://michaelscheiwiller.com/blog/setting-up-new-server">how to setup a vps</a> and <a href="https://michaelscheiwiller.com/blog/configure-ssh-config">how to configure ssh for your vps</a> so you can easily handle your vps.</p><p><strong>2. Systemd Service Configuration</strong></p><p>Create a systemd service for process management and automatic restarts:</p><p>File: <em>/etc/systemd/system/daily-stoic.service</em></p><pre>[Unit]<br>Description=Daily Stoic Website<br>Documentation=https://github.com/lappemic/366dailystoic<br>After=network.target<br><br>[Service]<br>Type=simple<br>User=devuser<br>WorkingDirectory=/home/devuser/projects/366dailystoic<br>ExecStart=/usr/bin/node server.js<br>Restart=on-failure<br>RestartSec=10<br>StandardOutput=syslog<br>StandardError=syslog<br>SyslogIdentifier=daily-stoic<br>Environment=NODE_ENV=production<br>Environment=PORT=3000<br><br># Security settings<br>NoNewPrivileges=yes<br>PrivateTmp=yes<br><br>[Install]<br>WantedBy=multi-user.target</pre><p><strong>Key Configuration Points:</strong></p><ul><li>User: Runs as non-root user (`devuser`) for security</li><li>WorkingDirectory: Absolute path to your project</li><li>Restart Policy: Automatically restarts on failure with 10-second delay</li><li>Logging: Outputs to syslog with identifier for easy monitoring</li><li>Security: Additional hardening with `NoNewPrivileges` and `PrivateTmp`</li></ul><p>Service Management:</p><pre># Enable and start the service<br>sudo systemctl daemon-reload<br>sudo systemctl enable daily-stoic<br>sudo systemctl start daily-stoic<br><br># Check status<br>sudo systemctl status daily-stoic<br><br># View logs<br>sudo journalctl -u daily-stoic -f</pre><p>For other systemd examples, check out the blog post <a href="https://michaelscheiwiller.com/blog/fast-private-streamlit-sharing-with-systemd">Fast Private Streamlit Sharing with Systemd</a></p><p><strong>3. Nginx Reverse Proxy Configuration</strong></p><p>Nginx serves as a reverse proxy, handling SSL termination, static file caching, and security headers.</p><p>File: <em>/etc/nginx/sites-available/daily-stoic</em></p><pre>server {<br>    server_name 366dailystoic.com www.366dailystoic.com;<br><br>    # Security headers<br>    add_header X-Frame-Options &quot;SAMEORIGIN&quot; always;<br>    add_header X-XSS-Protection &quot;1; mode=block&quot; always;<br>    add_header X-Content-Type-Options &quot;nosniff&quot; always;<br>    add_header Referrer-Policy &quot;no-referrer-when-downgrade&quot; always;<br>    add_header Content-Security-Policy &quot;default-src &#39;self&#39;; script-src &#39;self&#39; &#39;unsafe-inline&#39;; style-src &#39;self&#39; &#39;unsafe-inline&#39; https://fonts.googleapis.com; font-src &#39;self&#39; https://fonts.gstatic.com; connect-src &#39;self&#39;&quot; always;<br><br>    # Gzip compression<br>    gzip on;<br>    gzip_vary on;<br>    gzip_min_length 1024;<br>    gzip_proxied expired no-cache no-store private;<br>    gzip_types<br>        text/plain<br>        text/css<br>        text/xml<br>        text/javascript<br>        application/json<br>        application/javascript<br>        application/xml+rss<br>        application/atom+xml<br>        image/svg+xml;<br><br>    # Cache static assets and proxy to Node.js<br>    location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {<br>        proxy_pass http://localhost:3000;<br>        proxy_http_version 1.1;<br>        proxy_set_header Host $host;<br>        proxy_set_header X-Real-IP $remote_addr;<br>        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;<br>        proxy_set_header X-Forwarded-Proto $scheme;<br><br>        expires 1y;<br>        add_header Cache-Control &quot;public, immutable&quot;;<br>    }<br><br>    # Proxy to Node.js app<br>    location / {<br>        proxy_pass http://localhost:3000;<br>        proxy_http_version 1.1;<br>        proxy_set_header Upgrade $http_upgrade;<br>        proxy_set_header Connection &#39;upgrade&#39;;<br>        proxy_set_header Host $host;<br>        proxy_set_header X-Real-IP $remote_addr;<br>        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;<br>        proxy_set_header X-Forwarded-Proto $scheme;<br>        proxy_cache_bypass $http_upgrade;<br><br>        # Timeouts<br>        proxy_connect_timeout 60s;<br>        proxy_send_timeout 60s;<br>        proxy_read_timeout 60s;<br>    }<br><br>    listen 443 ssl; # managed by Certbot -&gt; this part will automatically be set after you run the certbot command in the next step<br>    ssl_certificate /etc/letsencrypt/live/366dailystoic.com/fullchain.pem;<br>    ssl_certificate_key /etc/letsencrypt/live/366dailystoic.com/privkey.pem;<br>    include /etc/letsencrypt/options-ssl-nginx.conf;<br>    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;<br>}<br><br># HTTP to HTTPS redirect<br>server {<br>    if ($host = www.366dailystoic.com) {<br>        return 301 https://$host$request_uri;<br>    }<br><br>    if ($host = 366dailystoic.com) {<br>        return 301 https://$host$request_uri;<br>    }<br><br>    listen 80;<br>    server_name 366dailystoic.com www.366dailystoic.com;<br>    return 404;<br>}</pre><p>Key Configuration Features:</p><ul><li>Security Headers: Comprehensive security headers for XSS, clickjacking, and content type protection</li><li>Gzip Compression: Reduces bandwidth usage for text-based files</li><li>Static File Caching: 1-year cache for CSS/JS files with immutable cache-control</li><li>Proxy Configuration: Proper headers for WebSocket support and real IP forwarding</li><li>SSL Configuration: Managed by Certbot for automatic certificate management</li></ul><p>Enable the Configuration:</p><pre># Create symlink to enable site<br>sudo ln -s /etc/nginx/sites-available/daily-stoic /etc/nginx/sites-enabled/<br><br># Test configuration<br>sudo nginx -t<br><br># Reload Nginx<br>sudo systemctl reload nginx</pre><p><strong>4. SSL Certificate Setup with Let’s Encrypt</strong></p><pre># Install Certbot<br>sudo apt install certbot python3-certbot-nginx -y<br><br># Obtain SSL certificate<br>sudo certbot - nginx -d 366dailystoic.com -d www.366dailystoic.com<br><br># Test automatic renewal<br>sudo certbot renew - dry-run</pre><h4><strong>Common Issues and Troubleshooting</strong></h4><p><strong>Issue 1: Static Files Return 404 Errors</strong></p><p><strong>Problem</strong>: CSS and JavaScript files return `404 Not Found` errors when accessed via the domain.</p><p><strong>Symptoms:</strong></p><pre>GET https://366dailystoic.com/style.css net::ERR_ABORTED 404 (Not Found)<br>GET https://366dailystoic.com/script.js net::ERR_ABORTED 404 (Not Found)</pre><p><strong>Root Cause</strong>: The Nginx location block for static files only sets cache headers but doesn’t include <em>proxy_pass</em> directive.</p><p><strong>Solution</strong>: Add the <em>proxy_pass</em> directive to the static files location block:</p><pre>location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {<br>    proxy_pass http://localhost:3000;  # This was missing!<br>    proxy_http_version 1.1;<br>    proxy_set_header Host $host;<br>    # ... other headers<br><br>    expires 1y;<br>    add_header Cache-Control &quot;public, immutable&quot;;<br>}</pre><p><strong>Verification</strong>:</p><pre># Test locally<br>curl -I http://localhost:3000/style.css<br><br># Test through Nginx<br>curl -I https://366dailystoic.com/style.css</pre><p><strong>Issue 2: Service Won’t Start</strong></p><p><strong>Common causes and solutions</strong>:</p><ul><li>Port conflicts: Ensure port 3000 isn’t used by another service</li><li>Permission issues: Verify the service user has read access to project files</li><li>Path errors: Double-check <em>WorkingDirectory</em> and <em>ExecStart</em> paths in service file</li><li>Missing dependencies: Run <em>npm install</em> in the project directory</li></ul><p><strong>Issue 3: Database Connection Errors</strong></p><p>Solution: Ensure the SQLite database file has proper permissions:</p><pre>sudo chown devuser:devuser daily-stoic.db<br>chmod 644 daily-stoic.db</pre><h4><strong>Performance Optimizations</strong></h4><p><strong>1. Database Query Optimization</strong></p><ul><li>Indexed date_key column for fast daily lookups</li><li>Connection pooling handled by sqlite3 driver</li><li>Graceful database connection management</li></ul><p><strong>2. Caching Strategy</strong></p><ul><li>Static files: 1-year browser cache with immutable flag</li><li>API responses: Consider adding ETag headers for future optimization</li><li>Nginx: Gzip compression for text-based content</li></ul><p><strong>3. Security Measures</strong></p><ul><li>Content Security Policy: Restricts resource loading to trusted sources</li><li>Security headers: Protection against common web vulnerabilities</li><li>HTTPS enforcement: All traffic redirected to secure connections</li><li>Process isolation: Service runs with minimal privileges</li></ul><h4><strong>Monitoring and Maintenance</strong></h4><p><strong>Service Monitoring</strong></p><pre># Check service status<br>sudo systemctl status daily-stoic<br><br># View real-time logs<br>sudo journalctl -u daily-stoic -f<br><br># Check Nginx status<br>sudo systemctl status nginx<br><br># Monitor resource usage<br>htop</pre><h4><strong>Conclusion</strong></h4><p>This setup provides a robust, production-ready deployment for a Node.js application with:</p><ul><li>High availability through systemd process management</li><li>Performance via Nginx reverse proxy and caching</li><li>Security through SSL, security headers, and proper permissions</li><li>Maintainability through clear configuration and logging</li></ul><p>The Daily Stoic website demonstrates that simple, well-architected solutions can be both powerful and maintainable. By leveraging proven technologies like Nginx, systemd, and Let’s Encrypt, we’ve created a deployment that’s secure, fast, and reliable.</p><p>The total setup takes about 30 minutes once you understand the components, and the result is a professional-grade web application that can handle significant traffic while remaining easy to maintain and monitor.</p><p><em>Want to build something similar for your team or business? </em><a href="/service/mailto:mi.scheiwiller@protonmail.com"><em>Contact me</em></a></p><p>This post was originally published in <a href="https://www.michaelscheiwiller.com/blog/deploy-nodejs-website-with-nginx-and-systemd-on-a-vps">michaelscheiwiller.com</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=4dc3fa7656bf" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Automating Streamlit App Deployment with GitHub Webhooks, Flask, Nginx and systemd]]></title>
            <link>https://medium.com/@mischeiwiller/automating-streamlit-app-deployment-with-github-webhooks-flask-nginx-and-systemd-308f8871070a?source=rss-2db7cef8c6a0------2</link>
            <guid isPermaLink="false">https://medium.com/p/308f8871070a</guid>
            <category><![CDATA[github-webhook]]></category>
            <category><![CDATA[nginx]]></category>
            <category><![CDATA[streamlit]]></category>
            <category><![CDATA[systemd]]></category>
            <category><![CDATA[flask]]></category>
            <dc:creator><![CDATA[Michael Scheiwiller]]></dc:creator>
            <pubDate>Thu, 12 Jun 2025 15:02:50 GMT</pubDate>
            <atom:updated>2025-06-17T05:31:32.684Z</atom:updated>
            <content:encoded><![CDATA[<p><em>How to automate your streamlit app deployment on your own vps</em></p><figure><img alt="Architecture of the automated streamlit deployment workflow" src="https://cdn-images-1.medium.com/max/673/1*jtvRHyZB1kdWia1ptRFHEA.png" /></figure><p><strong>This is the third post in a series on building and deploying a RAG Streamlit app. </strong>In this post, I cover how to automate the deployment. In the first post, I explained how to build the app itself, and in the second i go through how to share the app without making the code public.</p><p><strong>Read the other posts here:</strong></p><ol><li><a href="https://medium.com/@mischeiwiller/building-a-streamlit-powered-rag-app-with-chromadb-and-openai-623a35dfe0ca">Building a Streamlit-Powered RAG App with ChromaDB and OpenAI</a></li><li><a href="https://medium.com/@mischeiwiller/fast-private-streamlit-sharing-with-systemd-5c9c">Fast Private Streamlit Sharing with Systemd</a></li></ol><p><strong># 1. Introduction</strong></p><p>In <a href="https://medium.com/@mischeiwiller/fast-private-streamlit-sharing-with-systemd-5c9cce652400">Fast Private Streamlit Sharing with Systemd</a> i described how to setup streamlit and how to share it on a VPS with your customer. The issue with this approach is, you have to manually update the codebase on the vps. If you just want to share a ‘final state’ then this might be ok. But if you want to iterate and get feedback then this gets annoying and timeconsuming. This is why i automated this with a github webhook, flask, nginx and systemd.</p><p><strong># 2. Prerequisits</strong></p><p>To be able to automate this you should have build or have access to:</p><p>- VPS (will be on Linux)</p><p>- Python, virtualenv and pip</p><p>- Nginx</p><p>- Your app hosted on github</p><p>- basic knowledge of Linux cli</p><p>- systemd</p><p><strong># 3. Project Structure Overview</strong></p><p>The structure of my app is as follows (parts not relevant for this post are marked (Optional)):</p><pre>├── app.py # Main Streamlit application<br>├── update_and_restart.sh # Script to pull latest code and restart Streamlit<br>├── webhook_listener.py # Flask app to handle GitHub webhooks<br>├── requirements.txt # Python dependencies<br>├── .env # (Optional) Environment variables for Streamlit/OpenAI<br>├── .venv/ # Python virtual environment<br>├── data/ # (Optional) Data files for your app<br>├── chroma_db/ # (Optional) ChromaDB vector store<br>├── scripts/ # (Optional) Helper scripts for data processing<br>└── README.md # Project documentation</pre><p><strong># 4. Deployment Workflow</strong></p><p>Below is an extensive chart on how the deployment workflow looks like. Broken down to the main parts:</p><p>1. The user pushes a new commit to the github repository</p><p>2. Github sends a webhook to the flask app</p><p>3. The flask app pulls the latest code from the github repository</p><p>4. The flask app restarts the streamlit app</p><p>In the next section each part is explained in detail.</p><figure><img alt="Architecture of the automated streamlit deployment workflow" src="https://cdn-images-1.medium.com/max/673/1*jtvRHyZB1kdWia1ptRFHEA.png" /></figure><p><strong># 5. Step-by-Step Setup</strong></p><p><strong>## 5.1 Setting Up the Streamlit App and Virtaul Environment</strong></p><p>Clone your streamlit app to your VPS, create a virtual environment and install the dependencies.</p><pre>git clone &lt;your-repo-url&gt;<br>cd &lt;your-repo-name&gt;<br>python -m venv .venv<br>source .venv/bin/activate<br>pip install -r requirements.txt</pre><p><strong>## 5.2 Update Script</strong></p><p>Create a bash script to pull the latest code from the github repository and restart the streamlit app (e.g. `update_and_restart.sh`). This script will be used to update the app on the VPS.</p><pre>#!/bin/bash<br><br># Update the repository<br>git pull<br><br># activate the virtual environment<br>source .venv/bin/activate<br><br># install the dependencies<br>pip install -r requirements.txt<br><br># restart the application<br>sudo systemctl restart streamlit</pre><p><strong>## 5.3 Configuring Environment Variables</strong></p><p>Create a `.env` file to store the environment variables for the streamlit app (e.g. `GITHUB_WEBHOOK_SECRET`).</p><pre>GITHUB_WEBHOOK_SECRET=your-github-webhook-secret</pre><p>&gt; You can generate a random secret for the github webhook secret with `python3 -c “import secrets; print(secrets.token_hex(32))”`</p><p><strong>## 5.4 Flask Webhook Listener</strong></p><p>Create a flask app to handle the github webhook (e.g. `webhook_listener.py`). This app will be used to pull the latest code from the github repository and restart the streamlit app.</p><pre>import hashlib<br>import hmac<br>import os<br>import subprocess<br><br>from flask import Flask, abort, request<br><br>app = Flask(__name__)<br><br>GITHUB_SECRET = os.environ.get(&quot;GITHUB_WEBHOOK_SECRET&quot;, &quot;&quot;).encode()<br><br>def verify_signature(payload, signature):<br>    mac = hmac.new(GITHUB_SECRET, msg=payload, digestmod=hashlib.sha256)<br>    expected = &quot;sha256=&quot; + mac.hexdigest()<br>    return hmac.compare_digest(expected, signature)<br><br>@app.route(&quot;/github-webhook&quot;, methods=[&quot;POST&quot;])<br>def github_webhook():<br>    signature = request.headers.get(&quot;X-Hub-Signature-256&quot;)<br>    if not signature or not verify_signature(request.data, signature):<br>        abort(403)<br>    event = request.headers.get(&quot;X-GitHub-Event&quot;)<br>    if event == &quot;push&quot;:<br>        payload = request.json<br>        if payload and payload.get(&quot;ref&quot;) == &quot;refs/heads/main&quot;:<br>            subprocess.Popen(<br>                [&quot;/bin/bash&quot;, &quot;/home/devuser/projects/test-rag/update_and_restart.sh&quot;]<br>            )<br>    return &quot;&quot;, 204<br><br>if __name__ == &quot;__main__&quot;:<br>    app.run(host=&quot;127.0.0.1&quot;, port=9000)</pre><p><strong>## 5.5 Managing the Flask Listener with systemd</strong></p><p>Create a systemd service to manage the flask listener (e.g. `streamlit-webhook-listener.service`). This service will be used to start the flask listener on boot. This file lives in `/etc/systemd/system/`.</p><pre>  [Unit]<br>  Description=Flask GitHub Webhook Listener<br><br>  [Service]<br>  User=devuser<br>  WorkingDirectory=/home/devuser/projects/test-rag<br>  Environment=GITHUB_WEBHOOK_SECRET=&#39;&lt;your-github-webhook-secret&gt;&#39;<br>  ExecStart=/home/devuser/projects/test-rag/.venv/bin/python3 webhook_listener.py<br>  Restart=always<br><br>  [Install]<br>  WantedBy=multi-user.target</pre><p><strong>## 5.6 Nginx as a Reverse Proxy</strong></p><p>Nginx is used as a reverse proxy to handle the incoming requests to the flask app. This is done to protect the flask app from direct access from the internet. Create a nginx configuration file:</p><pre>sudo vim /etc/nginx/sites-available/streamlit-webhook</pre><p>and then add the following content:</p><pre>server {<br>    listen 8080;<br>    server_name YOUR_VPS_IP;<br><br>    location /github-webhook {<br>        proxy_pass http://127.0.0.1:9000/github-webhook;<br>        proxy_set_header Host $host;<br>        proxy_set_header X-Real-IP $remote_addr;<br>        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;<br>        proxy_set_header X-Forwarded-Proto $scheme;<br>    }<br>}</pre><p>The configuration file is then linked to the nginx sites-enabled directory:</p><pre>sudo ln -s /etc/nginx/sites-available/streamlit-webhook /etc/nginx/sites-enabled/<br>sudo nginx -t # test the configuration<br>sudo systemctl restart nginx</pre><p>**Test if the nginx configuration is working**</p><p>From your VPS try the following commands:</p><pre>sudo systemctl status nginx<br>sudo netstat -tuln | grep 8080</pre><p>You should see the following output:</p><pre>tcp 0 0 0.0.0.0:8080 0.0.0.0:* LISTEN</pre><p>and then try to send a request to the flask app:</p><pre>curl -X POST http://127.0.0.1:9000/github-webhook<br>curl -X POST http://localhost:8080/github-webhook</pre><p>- The first command should hit flask directly and return some html with an 403 error (not authorized)</p><p>- The second command should hit nginx and return as well some html with an 403 error (not authorized)</p><p>&gt; 1. A reverse proxy is a server that sits in front of your application and handles the incoming requests. It can be used to protect your application from direct access from the internet. (in contrast to a forward proxy which is used to access the internet)</p><p>&gt; 2. You can find the IP of your VPS with `curl -4 ifconfig.me`</p><p><strong>## 5.7 Open your Firewall</strong></p><p>If you are using a VPS provider like Hetzner, you might need to open the port 8080 in the firewall.</p><pre>sudo ufw allow 8080/tcp<br>sudo ufw enable</pre><p><strong>## 5.8 Github Webhook</strong></p><p>Create a github webhook to send a request to the flask app when a new commit is pushed to the repository.</p><p>1. Go to your github repository</p><p>2. Click on `Settings` &gt; `Webhooks` &gt; `Add Webhooks`</p><p>3. Set the payload URL to `http://YOUR_VPS_IP:8080/github-webhook`</p><p>4. Set the content type to `application/json`</p><p>5. Disable SSL verification</p><p>6. Set the secret to the same secret as in the `.env` file</p><p>7. Set the events to `Push`</p><p>8. Click on `Add webhook`</p><p><strong># 6. Test the Setup</strong></p><p>Push a new commit to the github repository and check if the streamlit app is updated.</p><p>- Is your vps reachable from the internet?</p><p>On your local machine try to send a request to the vps:</p><pre>curl -X POST http://YOUR_VPS_IP:8080/github-webhook</pre><p>If you get a 403 error again, you should be good to go.</p><p><strong># 7. Conclusion</strong></p><p>In this blog post i showed how to automate the deployment of a streamlit app on a VPS with github webhooks, flask, nginx and systemd. This is a great way to deploy your streamlit app and keep it up to date so you can iterate and get feedback from your customers.</p><p><em>Want to build something similar for your team or business? </em><a href="/service/mailto:mi.scheiwiller@protonmail.com"><em>Let’s talk</em></a><em> i am happy to help out</em></p><p><strong># 8. Resources</strong></p><ul><li><a href="https://michaelscheiwiller.com//blog/setting-up-new-server">Setting up a new VPS</a></li><li><a href="https://docs.streamlit.io/">Streamlit Documentation</a></li><li><a href="https://flask.palletsprojects.com/">Flask Documentation</a></li><li><a href="https://nginx.org/en/docs/">Nginx Documentation</a></li><li><a href="https://www.freedesktop.org/software/systemd/man/systemd.service.html">Systemd Documentation</a></li><li><a href="https://docs.github.com/en/webhooks">Github Webhooks Documentation</a></li></ul><p>This post was originally published in <a href="https://www.michaelscheiwiller.com/blog/streamlit-auto-deploy-github-webhook-flask-nginx-systemd">michaelscheiwiller.com</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=308f8871070a" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Fast Private Streamlit Sharing with Systemd]]></title>
            <link>https://medium.com/@mischeiwiller/fast-private-streamlit-sharing-with-systemd-5c9cce652400?source=rss-2db7cef8c6a0------2</link>
            <guid isPermaLink="false">https://medium.com/p/5c9cce652400</guid>
            <category><![CDATA[streamlit]]></category>
            <category><![CDATA[prototyping]]></category>
            <category><![CDATA[systemd]]></category>
            <category><![CDATA[linux]]></category>
            <dc:creator><![CDATA[Michael Scheiwiller]]></dc:creator>
            <pubDate>Tue, 03 Jun 2025 09:45:11 GMT</pubDate>
            <atom:updated>2025-06-17T05:25:39.961Z</atom:updated>
            <content:encoded><![CDATA[<p><em>How to run a Streamlit app as a systemd service on a VPS</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/647/1*0jGzzbOx_TFoYq6KQh4tnA.png" /><figcaption>systemd streamlit.service file content to share streamlit app</figcaption></figure><p><strong>This is the second post in a series on building and deploying a RAG Streamlit app. </strong>In this post, I cover how to share the app without making the code public. In the first posts, I explained how to build the app itself, and in the last i go through how to automate deployment.</p><p><strong>Read the other posts here:</strong></p><ol><li><a href="https://medium.com/@mischeiwiller/building-a-streamlit-powered-rag-app-with-chromadb-and-openai-623a35dfe0ca">Building a Streamlit-Powered RAG App with ChromaDB and OpenAI</a></li><li><a href="https://medium.com/@mischeiwiller/automating-streamlit-app-deployment-with-github-webhooks-flask-nginx-and-systemd-308f8871070a">Automating Streamlit App Deployment with Github Webhooks, Flask, Nginx ans systemd</a></li></ol><p>If you want to share a Streamlit app with someone who is not technical and can not clone and build the app on their own, you have to provide them somehow with a link they can access to use the app and give you feedback. For Streamlit apps whichs repos are public, this is easily done with <a href="https://streamlit.io/cloud">Streamlit Cloud</a>.</p><p>But if you want to keep the repo private, you have to host the app somewhere. One of the easiest ways to do this i found to use a VPS and run the app as a systemd service (check t<a href="https://medium.com/@mischeiwiller/setting-up-a-server-for-cronjobs-a-beginners-guide-to-cloud-hosting-ssh-and-github-access-0060b3b8542e">his post</a> for a vps setup guide). This post details how i set it up.</p><h3><strong>Running as a systemd Service</strong></h3><p>To keep the Streamlit app running in the background and automatically restart it if it fails, you can set it up as a systemd service.</p><p>1. Create the service file directly in `/etc/systemd/system`:</p><pre>sudo vim /etc/systemd/system/streamlit.service</pre><p>2. Paste the following content (edit paths as needed):</p><pre>[Unit]<br>Description=Streamlit App<br>After=network.target<br><br>[Service]<br>User=devuser<br>WorkingDirectory=/home/devuser/projects/test-rag<br>ExecStart=/home/devuser/projects/test-rag/.venv/bin/streamlit run app.py --server.port 8501 --server.address 0.0.0.0<br>Restart=always<br><br>[Install]<br>WantedBy=multi-user.target</pre><p>3. Enable and start the service:</p><pre>sudo systemctl daemon-reload<br>sudo systemctl enable streamlit<br>sudo systemctl start streamlit</pre><p>4. Check the status:</p><pre>sudo systemctl status streamlit</pre><p>The app will now run in the background and restart automatically if it crashes. You can access it at `http://YOUR_VPS_IP:8501/`.</p><p><em>Want to build something similar for your team or business? </em><a href="https://michaelscheiwiller.com/"><em>Contact me</em></a></p><p>This post was originally published in <a href="https://www.michaelscheiwiller.com/blog/fast-private-streamlit-sharing-with-systemd">michaelscheiwiller.com</a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=5c9cce652400" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Building a Streamlit-Powered RAG App with ChromaDB and OpenAI]]></title>
            <link>https://medium.com/@mischeiwiller/building-a-streamlit-powered-rag-app-with-chromadb-and-openai-623a35dfe0ca?source=rss-2db7cef8c6a0------2</link>
            <guid isPermaLink="false">https://medium.com/p/623a35dfe0ca</guid>
            <category><![CDATA[chromadb]]></category>
            <category><![CDATA[streamlit]]></category>
            <category><![CDATA[chatbots]]></category>
            <category><![CDATA[langchain]]></category>
            <dc:creator><![CDATA[Michael Scheiwiller]]></dc:creator>
            <pubDate>Tue, 03 Jun 2025 09:37:39 GMT</pubDate>
            <atom:updated>2025-06-17T05:22:34.922Z</atom:updated>
            <content:encoded><![CDATA[<p><em>How to build a Chatbot that allows users to ask questions about law documents</em></p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*9o2cv232CHl6xnsM" /><figcaption>Photo by <a href="https://unsplash.com/@coopery?utm_source=medium&amp;utm_medium=referral">Mohamed Nohassi</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p><strong>This is the first post in a series on building and deploying a RAG Streamlit app. </strong>In this post, I cover how to build the app itself. In the following posts, I explain how to share it without making the code public, and how to automate deployment.</p><p><strong>Read the other posts here:</strong></p><ol><li><a href="https://medium.com/@mischeiwiller/fast-private-streamlit-sharing-with-systemd-5c9cce652400">Fast Private Streamlit Sharing with Systemd</a></li><li><a href="https://medium.com/@mischeiwiller/automating-streamlit-app-deployment-with-github-webhooks-flask-nginx-and-systemd-308f8871070a">Automating Streamlit App Deployment with Github Webhooks, Flask, Nginx ans systemd</a></li></ol><h3><strong>1. Introduction</strong></h3><p>A friend of mine regularly runs into legal questions at work. These questions come from his clients. The thing is, he doesn’t have a legal background, and it’s not really his job to deal with these issues. But navigating laws and their connections gets messy fast. And if it’s not your core job, there’s almost no way to build up the experience to handle them confidently.</p><p>That’s why he, his team, and probably others rely on a lawyer to get answers. As you can imagine, these lawyers have a heavy workload, and sometimes it takes weeks to get a response.</p><p>When he told me about it, I figured this could be a great opportunity to dive into RAG (Retrieval-Augmented Generation) and build an MVP. So I started working on a RAG chatbot. This post is the first iteration, and there will likely be more as the app evolves and becomes more sophisticated.</p><h3><strong>2. What is Retrieval-Augmented Generation (RAG)?</strong></h3><p>LLM’s like ChatGPT have a vast amount of general knowledge. They can help you with everyday tasks in almost every domain. Regarding very specific and detailed knowledge they might have seen before but might have “problems” to remember. I imagine this has having kind of a “foggy mind” where they know the general direction but not every detail of it. In legal documents like laws these might be e.g. the number of a relevant article or the exact wording of a relevant passage.</p><p>This “foggy mind” shortcoming can be overcome with Retrieval-Augmented Generation (RAG). So the LLM does not have to answer out of memory but has a storage with the relevant documents and looks the information up. RAG is a hybrid architecture that has two stages:</p><p>1. **Retriever**: Given a query from a user the retriever searches the (document) storage to get the most relevant context (often via vector similarity using embeddings).</p><p>2. **Generator**: The retrieved context from stage 1 then is passed to an LLM, which uses it to base their answer on it.</p><h3><strong>3. Frameworks</strong></h3><p>I wanted to get something up and running as fast and simple as possible. This is why i went for a python only stack and no cloud database. This is what i used:</p><p>- <strong>Streamlit</strong> — <a href="https://streamlit.io/">Streamlit</a> is a fast and light python frontend framework. It lets you easily generate a simple UI. It is mostly used to showcase early MVP’s or internal Dashboards. It lacks a lot of configuration options and SEO possibilities, but for a fast iteration without overhead it is perfect.</p><p>- <strong>ChromaDB</strong> — <a href="https://www.trychroma.com/">ChromaDB</a> is an open source vector database on top of Sqlite. It also writes in a single file and can therefore easily be selfhosted on a vps (check <a href="https://medium.com/@mischeiwiller/setting-up-a-server-for-cronjobs-a-beginners-guide-to-cloud-hosting-ssh-and-github-access-0060b3b8542e">this blogpost</a>) to see how to set up a vps). It is designed for storing and querying embeddings.</p><p>- <strong>OpenAI</strong> <strong>API</strong> — Well, it is just easiest and for an MVP i did not bother setting up a local LLM. The <a href="https://platform.openai.com/docs/overview">OpenAI API</a> is used as the main LLM as well as for the Retriever and Generator.</p><h3><strong>4. Architecture</strong></h3><p>This is how the different pieces fit together:</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/631/1*AhdzyEA-IcvKa5PlmamSNA.png" /><figcaption>Architecture Overview of the Streamlit RAG app with ChromaDB and OpenAI API</figcaption></figure><p><strong>How it works:</strong></p><ol><li>The user asks a question in the Streamlit App</li><li>The app retrieves relevant law passages and articles form ChromaDB</li><li>The app sends the context and question to OpenAI</li><li>The LLM generates an answer, citing sources</li><li>The answer and sources are shown to the user</li></ol><p><a href="https://www.langchain.com/">LangChain</a> orchestrates the workflow:</p><ul><li>It takes the user’s question and retrieved context</li><li>Formats them into a prompt using a template</li><li>Sends this prompt to OpenAI</li><li>Parses and returns the answer</li></ul><h3><strong>5. Challenges &amp; Learnings</strong></h3><p>Since i wanted to be able to cite the exact article from a certain law, i had to store the articles as chunks. Where the chunks would be too long i had to separate the chunk with the same article. Storing this together with the law as metadata in ChromaDB was initially not trivial. Especially having different laws and different formats had to make me different chunking functions.</p><p>Building such applications has gotten much easier in the age of coding agents and IDE’s (i only use cursor these days). But putting together different frameworks and libraries for this RAG app did not work by only prompting. Especially the metada storing in ChromaDB (which references the law and article number) was a struggle in the beginning. But once you go oldschool and visit the docs you are good to go.</p><p><strong>Want to build something similar for your team or business? </strong><a href="https://www.michaelscheiwiller.com/"><strong>Contact me</strong></a></p><p>This post was originally published in <a href="https://www.michaelscheiwiller.com/blog/legal-rag-streamlit-chromadb-openai">michaelscheiwiller.com</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=623a35dfe0ca" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Cronjobs: A Beginner’s Guide to Scheduling Tasks on Your Server]]></title>
            <link>https://medium.com/@mischeiwiller/cronjobs-a-beginners-guide-to-scheduling-tasks-on-your-server-bb6f3aa05c19?source=rss-2db7cef8c6a0------2</link>
            <guid isPermaLink="false">https://medium.com/p/bb6f3aa05c19</guid>
            <category><![CDATA[cronjob]]></category>
            <dc:creator><![CDATA[Michael Scheiwiller]]></dc:creator>
            <pubDate>Mon, 31 Mar 2025 13:08:28 GMT</pubDate>
            <atom:updated>2025-03-31T13:08:28.577Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*3CVyK0e13njvFYP6" /><figcaption>Photo by <a href="https://unsplash.com/@ilyapavlov?utm_source=medium&amp;utm_medium=referral">Ilya Pavlov</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p><em>Cronjobs are a way to schedule any jobs on your machine/server: run a script, remove files, copy data, make backups. It is a very convenient way to actually automate regular and tedious work. As explained motivation to do so, was the jobscraper of </em><a href="https://www.nextgenenergyjobs.com/"><em>NextGenEnergyJobs.com</em></a><em>.</em></p><blockquote>Sidenote: if you need to set up a server first i also wrote</blockquote><blockquote><a href="https://medium.com/@mischeiwiller/setting-up-a-server-for-cronjobs-a-beginners-guide-to-cloud-hosting-ssh-and-github-access-0060b3b8542e">- How to set up a server</a> and</blockquote><blockquote><a href="https://medium.com/@mischeiwiller/simplify-server-access-a-beginners-guide-to-configuring-your-ssh-config-file-211e2d2a2142">- How to set up your ssh config file</a></blockquote><p>I first thought this is going to be straight forward: I mean we have llm’s these days. But i actually ended up running into troubles with the setup and then always postboning. This is why i wanted to write it down so i get it right from the beginning the next time and maybe it is helpful for others as well.</p><p>So first things first: open the crontab file (on your server or unix machine with)</p><pre>crontab -e</pre><p>which opens the crontab file in edit mode in your defined texteditor (vim for me). The content then looks like this:</p><pre># Edit this file to introduce tasks to be run by cron.<br>#<br># Each task to run has to be defined through a single line<br># indicating with different fields when the task will be run<br># and what command to run for the task<br>#<br># To define the time you can provide concrete values for<br># minute (m), hour (h), day of month (dom), month (mon),<br># and day of week (dow) or use &#39;*&#39; in these fields (for &#39;any&#39;).<br>#<br># Notice that tasks will be started based on the cron&#39;s system<br># daemon&#39;s notion of time and timezones.<br>#<br># Output of the crontab jobs (including errors) is sent through<br># email to the user the crontab file belongs to (unless redirected).<br>#<br># For example, you can run a backup of all your user accounts<br># at 5 a.m every week with:<br># 0 5 * * 1 tar -zcf /var/backups/home.tgz /home/<br>#<br># For more information see the manual pages of crontab(5) and cron(8)<br>#<br># m h dom mon dow command</pre><p>So the content itself actually already tells us a lot about cron and cronjobs: The first 5 entries of the line are whitespace separated integers which indicate the</p><ul><li>minutes</li><li>hour</li><li>day of month</li><li>month and</li><li>day of week</li></ul><p>For any of these entries you also can use ‘\*’ for ‘any’. After these schedule indicators come the actual command. What i first not was aware of, is that cronjobs run with a limited environment, and the `PATH` variable might not include the directories needed for e.g. `python` or even `echo`. What i first came up with is a line of code to change into the scraper directory, activate the virtual environement and start the scraper script. Since this failed, i then did the following to test (as said cronjobs run in a limited environment, so its better to write the full paths):</p><pre>* * * * * /bin/echo &quot;Successful cronjob&quot; &gt;&gt; /home/scraperuser/cronjobtest.log</pre><p>This runs every minute and appends “Successful cronjob” to the specified `cronjobtest.log` file. With this running i then realised i need to call the bash shell specifically. So the final crontab entry for the scraper was:</p><pre>0 9 * * * /bin/bash -c &#39;cd /home/scraperuser/projects/best-jobscraper-in-the-world &amp;&amp; source .venv/bin/activate &amp;&amp; python3 src/main.py&#39; &gt;&gt; /home/scraperuser/jobscraper.log 2&gt;&amp;1</pre><p>This cronjob runs everyday at 09:00 am UTC and executes from the bash shell the command in the quotes’ and appends it to the `jobscraper.log` file.</p><p><em>Originally posted on </em><a href="https://www.michaelscheiwiller.com/blog/working-with-cronjobs"><em>michaelscheiwiller.com</em></a></p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=bb6f3aa05c19" width="1" height="1" alt="">]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Simplify Server Access: A Beginner’s Guide to Configuring Your SSH Config File]]></title>
            <link>https://medium.com/@mischeiwiller/simplify-server-access-a-beginners-guide-to-configuring-your-ssh-config-file-211e2d2a2142?source=rss-2db7cef8c6a0------2</link>
            <guid isPermaLink="false">https://medium.com/p/211e2d2a2142</guid>
            <category><![CDATA[ssh-config]]></category>
            <category><![CDATA[server-access]]></category>
            <category><![CDATA[ssh]]></category>
            <dc:creator><![CDATA[Michael Scheiwiller]]></dc:creator>
            <pubDate>Mon, 31 Mar 2025 06:46:30 GMT</pubDate>
            <atom:updated>2025-03-31T06:46:30.497Z</atom:updated>
            <content:encoded><![CDATA[<figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/0*zcIgg3KPHjrFTW2F" /><figcaption>Photo by <a href="https://unsplash.com/@flowforfrank?utm_source=medium&amp;utm_medium=referral">Ferenc Almasi</a> on <a href="https://unsplash.com/?utm_source=medium&amp;utm_medium=referral">Unsplash</a></figcaption></figure><p>I’ve been working more with servers lately, and that means getting into SSH. One thing that i found annoying was to type out full IP addresses every time I needed to connect, like `ssh root@49.31.40.21`. It’s annoying and likely for errors. Then I discovered the SSH config file. It lets you give your servers friendly names, use tab autocomplete, and connect in seconds. Here’s how to set it up, step by step.</p><h4>1. Open (or Create) Your SSH Config File</h4><p>On your local machine, head to your SSH directory and open — or create — the config file:</p><pre>vim ~/.ssh/config</pre><p>If it doesn’t exist yet, Vim will create it for you.</p><h4>2. Add Your Server Details</h4><p>Now, add entries for each server and user combo you want to connect to. For example, I manage a Hetzner server with two users: `root` and `scraperuser`. The latter runs cronjobs and hosts job-scraping repos for <a href="https://nextgenenergyjobs.com/">NextGenEnergyJobs.com</a>. (Need help setting up a server? Check out <a href="https://medium.com/@mischeiwiller/setting-up-a-server-for-cronjobs-a-beginners-guide-to-cloud-hosting-ssh-and-github-access-0060b3b8542e">my previous post on setting up a new Linux server</a>).</p><p>Here’s what I added to my config:</p><pre>Host hetzner-root<br> HostName 71.13.43.01<br> IdentityFile ~/.ssh/id_ed25519_hetzner<br> User root<br><br>Host hetzner-scraper<br> HostName 71.13.43.01<br> IdentityFile ~/.ssh/id_ed25519_hetzner<br> User scraperuser</pre><ul><li>`Host`: Your custom nickname for the connection (e.g., `hetzner-root`)</li><li>`HostName`: The server’s IP address</li><li>`IdentityFile`: Path to the SSH private key (make sure it matches your key pair)</li><li>`User`: The username on the server.</li></ul><h4>3. Save and Connect</h4><p>Save the file (in Vim, hit `:wq` and Enter), and you are ready. Now, connecting is as simple as:</p><pre>ssh hetzner-root<br><br># or<br>ssh hetzner-scraper</pre><p>If you type `ssh hetz` and hit Tab — your shell will autocomplete the rest.</p><p>Let me know what you think! I am very happy for every feedback!</p><p>This post was originally published on <a href="https://www.michaelscheiwiller.com/blog/configure-ssh-config">michaelscheiwiller.com</a>.</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=211e2d2a2142" width="1" height="1" alt="">]]></content:encoded>
        </item>
    </channel>
</rss>