<?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>Howto on MoskitoHero</title><link>/tags/howto/</link><description>Recent content in Howto on MoskitoHero</description><generator>Hugo</generator><language>en-us</language><lastBuildDate>Tue, 06 May 2025 00:00:00 +0000</lastBuildDate><atom:link href="/tags/howto/index.xml" rel="self" type="application/rss+xml"/><item><title>Set up Ghost with Podman on a VPS</title><link>/post/2025-05-06-set-up-ghost-with-podman-on-a-vps/</link><pubDate>Tue, 06 May 2025 00:00:00 +0000</pubDate><guid>/post/2025-05-06-set-up-ghost-with-podman-on-a-vps/</guid><description>A step-by-step walkthrough for running Ghost on a VPS using Podman quadlets backed by Systemd, with MySQL and Caddy as a reverse proxy.</description><content:encoded><![CDATA[<div class="paragraph">
<p>I recently moved this website / blog from Jekyll to <a href="https://ghost.org/">Ghost</a>, just to try it out after hearing quite a lot of nice buzz about it lately.
I had this small personal VPS at hand, so I decided to give it some space there, and use <a href="https://podman.io/">Podman</a> (and its <em>quadlets</em> feature) to set it up. This leverages Systemd to run your docker images. Here is a quick walkthrough, should anyone be interested:
Obviously, you need a VPS with SSH access. It must have its firewall open to the web on port 443. My VPS is running Ubuntu, so my install commands will reflect this. I also chose to use root mode to run the quadlets, but there are other options available.</p>
</div>
<div class="sect2">
<h3 id="setup"><a class="anchor" href="#setup"></a>Setup</h3>
<div class="paragraph">
<p>First, <a href="https://podman.io/docs/installation">install Podman</a>:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="shell"><span class="nb">sudo </span>apt <span class="nb">install </span>podman podman-docker podman-toolbox</code></pre>
</div>
</div>
<div class="paragraph">
<p>Pull the required images from docker:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="shell"><span class="nb">sudo </span>podman pull docker.io/mysql:8
<span class="nb">sudo </span>podman pull docker.io/ghost:5-alpine
<span class="nb">sudo </span>podman pull docker.io/caddy:alpine</code></pre>
</div>
</div>
</div>
<div class="sect1">
<h2 id="the-systemd-services"><a class="anchor" href="#the-systemd-services"></a>The Systemd services</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Create a MySQL container file at /etc/containers/systemd/mysql.container with this content:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="shell"><span class="o">[</span>Unit]
<span class="nv">Description</span><span class="o">=</span>Blog MySQL Container
<span class="nv">After</span><span class="o">=</span>network-online.target

<span class="o">[</span>Container]
<span class="nv">ContainerName</span><span class="o">=</span>blog-mysql
<span class="nv">AddCapability</span><span class="o">=</span>SYS_NICE
<span class="nv">Image</span><span class="o">=</span>docker.io/mysql:8
<span class="nv">Volume</span><span class="o">=</span>/srv/containers/blog/mysql/var/lib/mysql:/var/lib/mysql:Z
<span class="nv">Pod</span><span class="o">=</span>blog.pod
<span class="nv">Secret</span><span class="o">=</span>blog_db_name,type<span class="o">=</span><span class="nb">env</span>,target<span class="o">=</span>MYSQL_DATABASE
<span class="nv">Secret</span><span class="o">=</span>blog_db_user,type<span class="o">=</span><span class="nb">env</span>,target<span class="o">=</span>MYSQL_USER
<span class="nv">Secret</span><span class="o">=</span>blog_db_password,type<span class="o">=</span><span class="nb">env</span>,target<span class="o">=</span>MYSQL_PASSWORD
<span class="nv">Secret</span><span class="o">=</span>blog_db_rootpassword,type<span class="o">=</span><span class="nb">env</span>,target<span class="o">=</span>MYSQL_ROOT_PASSWORD

<span class="o">[</span>Service]
<span class="nv">Restart</span><span class="o">=</span>always

<span class="o">[</span>Install]
<span class="nv">WantedBy</span><span class="o">=</span>default.target
<span class="nv">RequiredBy</span><span class="o">=</span>blog-ghost.service</code></pre>
</div>
</div>
<div class="paragraph">
<p>Create a Ghost container file at /etc/containers/systemd/ghost.container
Do not forget to change yourdomain.com with your domain.</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="shell"><span class="o">[</span>Unit]
<span class="nv">Description</span><span class="o">=</span>Ghost Container
<span class="nv">After</span><span class="o">=</span>blog-db.service

<span class="o">[</span>Container]
<span class="nv">ContainerName</span><span class="o">=</span>blog-ghost
<span class="nv">Environment</span><span class="o">=</span><span class="nv">database__client</span><span class="o">=</span>mysql <span class="nv">database__connection__host</span><span class="o">=</span>blog-mysql <span class="nv">url</span><span class="o">=</span>http://yourdomain.com
<span class="nv">Image</span><span class="o">=</span>docker.io/ghost:5-alpine
<span class="nv">Pod</span><span class="o">=</span>blog.pod
<span class="nv">Secret</span><span class="o">=</span>blog_db_name,type<span class="o">=</span><span class="nb">env</span>,target<span class="o">=</span>database__connection__database
<span class="nv">Secret</span><span class="o">=</span>blog_db_user,type<span class="o">=</span><span class="nb">env</span>,target<span class="o">=</span>database__connection__user
<span class="nv">Secret</span><span class="o">=</span>blog_db_password,type<span class="o">=</span><span class="nb">env</span>,target<span class="o">=</span>database__connection__password
<span class="nv">Volume</span><span class="o">=</span>/srv/containers/blog/var/lib/ghost/content:/var/lib/ghost/content:Z

<span class="o">[</span>Service]
<span class="nv">Restart</span><span class="o">=</span>always

<span class="o">[</span>Install]
<span class="nv">WantedBy</span><span class="o">=</span>default.target</code></pre>
</div>
</div>
<div class="paragraph">
<p>You need to set up the secrets used in these two files to access MySQL. Let’s do it:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="shell"><span class="nb">printf</span> <span class="si">$(</span><span class="nb">tr</span> <span class="nt">-dc</span> A-Za-z0-9 &lt;/dev/urandom | <span class="nb">head</span> <span class="nt">-c</span> 13<span class="si">)</span> | <span class="nb">sudo </span>podman secret create blog_db_rootpassword -
<span class="nb">printf</span> <span class="si">$(</span><span class="nb">tr</span> <span class="nt">-dc</span> A-Za-z0-9 &lt;/dev/urandom | <span class="nb">head</span> <span class="nt">-c</span> 13<span class="si">)</span> | <span class="nb">sudo </span>podman secret create blog_db_password -
<span class="nb">printf</span> <span class="s2">&#34;gdb&#34;</span> | <span class="nb">sudo </span>podman secret create blog_db_name -
<span class="nb">printf</span> <span class="s2">&#34;guser&#34;</span> | <span class="nb">sudo </span>podman secret create blog_db_user -</code></pre>
</div>
</div>
<div class="paragraph">
<p>You also need to have the files stored on your filesystem, so let’s create the directories that are mounted in the images:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="shell"><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /srv/containers/blog/var/lib/ghost/content
<span class="nb">sudo mkdir</span> <span class="nt">-p</span> /srv/containers/blog/mysql/var/lib/mysql</code></pre>
</div>
</div>
<div class="paragraph">
<p>Now, add a blog.network file at /etc/containers/systemd/blog.network</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="shell"><span class="o">[</span>Unit]
<span class="nv">Description</span><span class="o">=</span>Custom blog Podman network

<span class="o">[</span>Network]
<span class="nv">NetworkName</span><span class="o">=</span>blog
<span class="nv">Driver</span><span class="o">=</span>bridge</code></pre>
</div>
</div>
<div class="paragraph">
<p>And a blog.pod file at /etc/containers/systemd/blog.pod</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="shell"><span class="o">[</span>Unit]
<span class="nv">Description</span><span class="o">=</span>Blog Pod

<span class="o">[</span>Pod]
<span class="nv">PodName</span><span class="o">=</span>blog
<span class="nv">Network</span><span class="o">=</span>blog.network
<span class="nv">PublishPort</span><span class="o">=</span>3001:2368</code></pre>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="caddy-set-up"><a class="anchor" href="#caddy-set-up"></a>Caddy set up</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Let’s use <a href="https://caddyserver.com/">Caddy</a> to serve the app to the internet. <em>I chose to leave Caddy out of the blog pod context because I use it to proxy other toy servers on that VPS.</em>
Create the service file at /etc/containers/systemd/caddy.container</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="shell"><span class="o">[</span>Container]
<span class="nv">ContainerName</span><span class="o">=</span>caddy
<span class="nv">Image</span><span class="o">=</span>docker.io/caddy:alpine
<span class="nv">PublishPort</span><span class="o">=</span>80:80
<span class="nv">PublishPort</span><span class="o">=</span>443:443
<span class="nv">Volume</span><span class="o">=</span>/srv/containers/caddy/Caddyfile:/etc/caddy/Caddyfile:ro
<span class="nv">Volume</span><span class="o">=</span>/srv/containers/caddy/data:/data
<span class="nv">Volume</span><span class="o">=</span>/srv/containers/caddy/config:/config
<span class="nv">Network</span><span class="o">=</span>host

<span class="o">[</span>Service]
<span class="nv">Restart</span><span class="o">=</span>always

<span class="o">[</span>Install]
<span class="nv">WantedBy</span><span class="o">=</span>default.target</code></pre>
</div>
</div>
<div class="paragraph">
<p>And create the Caddy directory:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="shell"><span class="nb">sudo mkdir</span> <span class="nt">-p</span> /srv/containers/caddy</code></pre>
</div>
</div>
<div class="paragraph">
<p>Add a Caddyfile at /srv/containers/caddy/Caddyfile with the following content:</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="shell"><span class="o">{</span>
    <span class="c"># email to use on Let&#39;s Encrypt</span>
    email your@email.com
<span class="o">}</span>

www.yourdomain.com <span class="o">{</span>
        redir https://yourdomain.com<span class="o">{</span>uri<span class="o">}</span>
<span class="o">}</span>

yourdomain.com <span class="o">{</span>
  reverse_proxy 127.0.0.1:3001
<span class="o">}</span></code></pre>
</div>
</div>
<div class="paragraph">
<p>Do not forget to update the file with your email and domain.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="go-live"><a class="anchor" href="#go-live"></a>Go live</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Now let’s open the gates…​</p>
</div>
<div class="listingblock">
<div class="content">
<pre class="rouge highlight"><code data-lang="shell"><span class="nb">sudo </span>systemctl daemon-reload
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>caddy
<span class="nb">sudo </span>systemctl start caddy
<span class="nb">sudo </span>systemctl <span class="nb">enable </span>blog-pod
<span class="nb">sudo </span>systemctl start blog-pod</code></pre>
</div>
</div>
<div class="paragraph">
<p>And visit yourdomain.com/ghost to start blogging!</p>
</div>
</div>
</div>
]]></content:encoded></item><item><title>Lessons learned after migrating from Heroku to a kubernetes cluster</title><link>/post/2021-02-20-lessons-learned-after-migrating-from-heroku-to-a-kubernetes-cluster/</link><pubDate>Sat, 20 Feb 2021 00:00:00 +0000</pubDate><guid>/post/2021-02-20-lessons-learned-after-migrating-from-heroku-to-a-kubernetes-cluster/</guid><description>I recently migrated my company’s rails hosting from Heroku to a kubernetes cluster. It was a daunting task, but I learned a lot. Here are the lessons I learned, and how you can avoid the pitfalls I encountered.</description><content:encoded><![CDATA[<div class="paragraph">
<p>Part of the mission I was given when I arrived at my now workplace was the complete migration of our rails hosting from a Heroku service to a kubernetes solution. This was my first experience migrating a production stack, and my first experience with kubernetes as well. And just in case you’d think the task wasn’t daunting enough, the previous sole developper decided to leave for another opportunity a few weeks after my arrival. This departure was soon forgotten with the company hiring another developper who had already transitioned an app to kubernetes. So I took a course on udemy, read the kubernetes documentation thouroughly, took notes, and played around with a toy cluster. Then I set out on the transition journey.</p>
</div>
<div class="paragraph">
<p>Six months later, the Heroku-Kubernetes transition is now over. I have gained a lot of experience. And I feel it’s time to share it.</p>
</div>
<div class="sect1">
<h2 id="the-stack"><a class="anchor" href="#the-stack"></a>The stack</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Our company’s flagship product is a smart lamp that detects elderly people’s movements and activities and send an alarm whenever the fall. It is mainly used in french retirement houses, but we have received quite a lot of buzz in the silver economy field and we are about to scale worldwide, and sell our product, along with its web infrastructure, to franchised resellers. This is where Kubernetes comes as a handy solution, as it allows for quick replication and scaling of a single stack.</p>
</div>
<div class="paragraph">
<p>So the stack consists in a MQTT broker (hosted with CloudMQTT) that receives the messages sent by the smart lamps and their accessories. A sidekiq worker is then constantly polling the broker to get the messages and take action based on the content: send alerts, monitor activity, or just log temperature and so on. The rails web app is in charge of an admin interface and a few API endoints used by the mobile apps. Other connected services include Heroku Postgres for the data, Redis as the Sidekiq backend, Elasticsearch to index and search the logged MQTT payloads, logentries to track the rails log, Sentry for alerting and Sendgrid for emails.</p>
</div>
<div class="paragraph">
<p>All these services had been subscribed to as Saas from the Heroku app console.</p>
</div>
</div>
</div>
<div class="sect1">
<h2 id="what-i-did-right"><a class="anchor" href="#what-i-did-right"></a>What I did right</h2>
<div class="sectionbody">
<div class="sect2">
<h3 id="i-asked-for-time"><a class="anchor" href="#i-asked-for-time"></a>I asked for time</h3>
<div class="paragraph">
<p>Given the context, that was the first thing I did. The company wanted a swift move, but I said I had to learn about the stack, get acquainted with the codebase and troubleshoot a few issues before I could get a good idea of what the implications of such a migration meant. They agreed, and gave me a few months to transition.</p>
</div>
<div class="paragraph">
<p>I can only imagine how painful it would have been to undertake such a task with a limited knowledge of the app.</p>
</div>
</div>
<div class="sect2">
<h3 id="i-asked-for-help"><a class="anchor" href="#i-asked-for-help"></a>I asked for help</h3>
<div class="paragraph">
<p>My previous devops-ish experience was limited to editing my Capistrano configuration and maintaining a few VPS. Pushing a new update at my new workplace merely consisted in typing <code>git push heroku master</code> and maybe run a migration on the server afterwards. Tuning the stack consisted in adding or removing a dyno, and changing the Saas billing plan to get more ressources.</p>
</div>
<div class="paragraph">
<p>When I started looking at the way Kubernetes is architectured, I quickly understood that I needed the help of a consultant to guide me and advise me on how to build the new stack and make wise decisions on the myriad of configuration options available. I am ever so thankful to that guy, who happened to be very friendly and patient. Kudos.</p>
</div>
</div>
<div class="sect2">
<h3 id="i-played-around-and-took-notes"><a class="anchor" href="#i-played-around-and-took-notes"></a>I played around and took notes</h3>
<div class="paragraph">
<p>This is important. I destroyed and rebuilt the cluster from scratch at least three times. Every time, I wrote down stuff in a wiki for reference. And every time, I improved the workflow and gained a lot of experience.</p>
</div>
<div class="paragraph">
<p>I deleted pods, services, recreated them, fiddled with PVCs, tried out many helm charts…​ This gave me hands-on ecxperience and allowed me to crash things a few times before knowing how to get things working.</p>
</div>
</div>
<div class="sect2">
<h3 id="i-improved-the-production-workflow"><a class="anchor" href="#i-improved-the-production-workflow"></a>I improved the production workflow</h3>
<div class="paragraph">
<p>Maybe I should say &#34;he&#34;, because that’s what the kubernetes consultant’s job.</p>
</div>
<div class="paragraph">
<p>We now have a nice, fully integrated CI flow that creates kubernetes environments for production, testing and staging. It still needs to be improved, but we are almost on par with <code>git push heroku master</code> now.</p>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="what-i-did-wrong"><a class="anchor" href="#what-i-did-wrong"></a>What I did wrong</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Lots of things. Here are the details.</p>
</div>
<div class="sect2">
<h3 id="i-lost-time-adapting-the-stack"><a class="anchor" href="#i-lost-time-adapting-the-stack"></a>I lost time adapting the stack</h3>
<div class="paragraph">
<p>When I introduced Elasticsearch to the stack, our memory consumption went ballistic. So we decided to find an alternative. Elasticsearch is not used as an end-user-facing feature. It is used by our aftersales department and our developers. So we decided that searching the logs could take 3 seconds instead of 300ms and got rid of elasticsearch altogether.</p>
</div>
<div class="paragraph">
<p>We were wrong. Not only did I spend a lot of time researching solutions, tuning indices, partitioning tables, refactoring code, but in the end I didn’t come up with a viable alternative that could be used on a day-to-day basis by my fellow workers. So I decided to temporarily move back to Elasticsearch to transition, and do the reasearch later on. That should have been our decision from the start, because we lost plenty of man-hour money for the sake of saving a few yearly hundred bucks.</p>
</div>
</div>
<div class="sect2">
<h3 id="i-added-features"><a class="anchor" href="#i-added-features"></a>I added features</h3>
<div class="paragraph">
<p>When you are given a new toy, it is tempting to try it in every situation that pops about.</p>
</div>
<div class="paragraph">
<p>Don’t.</p>
</div>
<div class="paragraph">
<p>After a few weeks, the k8s branch had diverged from master in an tremendous measure. Again, it could have gone horribly wrong, and it did to some extent, as I had to do a few live patches on day 2 after a few customers explaind that some parts of the service was not behaving as expacted.</p>
</div>
</div>
<div class="sect2">
<h3 id="i-changed-versions"><a class="anchor" href="#i-changed-versions"></a>I changed versions</h3>
<div class="paragraph">
<p>Hey. Postgres 12 is available. Let’s use it.</p>
</div>
<div class="paragraph">
<p>Maybe that’s a good idea to upgrade your database. But just keep this for a later time, once the dust is settled. I did encounter a few bugs related to that upgrade. They were not gigormous bugs. But they could have been avoided, or postponed.</p>
</div>
</div>
<div class="sect2">
<h3 id="i-moved-everything-in-one-go"><a class="anchor" href="#i-moved-everything-in-one-go"></a>I moved everything in one go</h3>
<div class="paragraph">
<p>We decided that we wanted to make our cluster totally self-contained and stop relying on Saas, except for Sentry and Sendgrid. So we went for Stolon as a high-availability Postgresql database, VerneMQ for the MQTT broker, and bitnami helm charts for Elasticsearch and Redis.</p>
</div>
<div class="paragraph">
<p>This is fine. But it is too much to handle in a single move. I had quite a lot of experience with handling a Postgres database, Redis is OK, but I was a complete newbie to elasticsearch administration. On migration day, this was the servoce that caused the most issues, and I had to reindex the 80 million logs database twice to get it right without crashing the server.</p>
</div>
<div class="paragraph">
<p>Looking back, things could have gone extremely wrong.</p>
</div>
<div class="paragraph">
<p>I should have taken things gradually:</p>
</div>
<div class="ulist">
<ul>
<li>
<p>First, the rails app, Redis and Postgres. That’s the core of our stack. And that’s enough work for a fist pass. Fix the bugs, tune the app, and let it settle for a week or two.</p>
</li>
<li>
<p>Second, set up Grafana. I use Loki for logs, so it is a nice replacement for Logentries.</p>
</li>
<li>
<p>Then, bring in the MQTT broker. It’s easy, because you can just bridge the messages from one to another. Then that’s just a case of pointing the devices to the new URL. That’s the IoT dev team’s problem, not mine.</p>
</li>
<li>
<p>Finally, try Elasticsearch with a subset of logs. See how it behaves. Then gradually move the rest to the new infrastrucure.</p>
</li>
</ul>
</div>
</div>
</div>
</div>
<div class="sect1">
<h2 id="tl-dr-how-you-should-do-it"><a class="anchor" href="#tl-dr-how-you-should-do-it"></a>TL; DR; How you should do it</h2>
<div class="sectionbody">
<div class="paragraph">
<p>Don’t be too greedy, be humble and try moving things little by little. Try to keep the stack as similar as possible as the Heroku stack to start with: same software versions, same environment. Only then can you upgrade software if you wish to do so. Keep the connected Saas running, and gradually add them to your stack if that’s what you want to do. But be sure to have a proper testing and staging environment beforehand.</p>
</div>
</div>
</div>
]]></content:encoded></item></channel></rss>