<?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: Josef Strzibny</title>
    <description>The latest articles on DEV Community by Josef Strzibny (@strzibny).</description>
    <link>https://dev.to/strzibny</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F154689%2F89190986-4d70-4c80-8958-ef6407185146.jpeg</url>
      <title>DEV Community: Josef Strzibny</title>
      <link>https://dev.to/strzibny</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/strzibny"/>
    <language>en</language>
    <item>
      <title>How to implement AI agents in Rails with RubyLLM</title>
      <dc:creator>Josef Strzibny</dc:creator>
      <pubDate>Fri, 24 Apr 2026 07:41:13 +0000</pubDate>
      <link>https://dev.to/serpapi/how-to-implement-ai-agents-in-rails-with-rubyllm-4fh7</link>
      <guid>https://dev.to/serpapi/how-to-implement-ai-agents-in-rails-with-rubyllm-4fh7</guid>
      <description>&lt;p&gt;Chat-based agents are augmented LLM interfaces with access to a list of predefined tools. RubyLLM Agents are reusable AI assistants implemented as models with their configuration, runtime context, and prompt conventions. Let's see how we can start implementing custom OpenAI chat agents with access to SERP tools with the help of the RubyLLM gem.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 Note the difference between fully autonomous agents like Claude Code or Codex, and chat-based agents that still react to user input. This post is about the latter.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Simple chats vs agents
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://rubyllm.com" rel="noopener noreferrer"&gt;RubyLLM&lt;/a&gt; is a Ruby gem and an AI interface for GPT, Claude, and Gemini to give us an easy way to run LLM chat inside a Ruby application. It allows us to avoid writing JSON and lets us work with AI using beautiful Ruby DSL.&lt;/p&gt;

&lt;p&gt;A regular RubyLLM chat is a conversation. A user sends a message, the model responds, and the exchange continues back and forth. It works but it's limited to what the model can do. Today's models can do way more than before as they often search the web and find up-to-date information. However, they still do a lot of guessing and cannot access your internal data. This means we need to be very specific and provide a lot of context for the LLM to understand our request.&lt;/p&gt;

&lt;p&gt;Imagine we ask an LLM something more complex with a simple sentence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Choose any model you like, just make sure to set up&lt;/span&gt;
&lt;span class="c1"&gt;# the access token in the initializator&lt;/span&gt;
&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="s2"&gt;"gpt-5.4-mini"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt; &lt;span class="s2"&gt;"Is our 'Aeropress Coffee Maker' at $39.99 competitive?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;How could this LLM model know the answer?&lt;/p&gt;

&lt;p&gt;Perhaps the agent could search our pages as well as our competitors' to gain a full understanding of our request. This usually requires some back and forth to ensure the LLM has all the context needed to give an accurate response. If we want more precision, access to application data, and better results, we need to give the chat agent access to the tools we control.&lt;/p&gt;

&lt;p&gt;Now imagine what would happen if the chat agent has access to a local database and Google Shopping:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;chat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;model: &lt;/span&gt;&lt;span class="s2"&gt;"gpt-5.4-mini"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;with_tools&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;LookupProduct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SearchGoogleShopping&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt; &lt;span class="s2"&gt;"Is our 'Aeropress Coffee Maker' competitively priced?"&lt;/span&gt;

&lt;span class="c1"&gt;# =&amp;gt; "Your Aeropress Coffee Maker is listed at $39.99. Here are current&lt;/span&gt;
&lt;span class="c1"&gt;# Google Shopping prices:&lt;/span&gt;
&lt;span class="c1"&gt;# 1. Amazon — $34.95&lt;/span&gt;
&lt;span class="c1"&gt;# 2. Target — $37.99&lt;/span&gt;
&lt;span class="c1"&gt;# 3. Walmart — $33.49&lt;/span&gt;
&lt;span class="c1"&gt;# 4. Williams Sonoma — $41.95&lt;/span&gt;
&lt;span class="c1"&gt;# 5. REI — $39.95&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# You're on the higher end. Three out of five retailers are under $38.&lt;/span&gt;
&lt;span class="c1"&gt;# Consider adjusting to ~$36-37 to stay competitive."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suddenly it has everything it needs to answer such an ambiguous question. It can search for competitors products on &lt;a href="https://serpapi.com/google-shopping-api" rel="noopener noreferrer"&gt;Google Shopping&lt;/a&gt; and compare it with data we have in our product catalog. Not bad at all.&lt;/p&gt;

&lt;p&gt;The concept of using tools is simple. We describe a set of tools to the model, each with a name, parameters, and what it does. When the model determines it needs specific information or wants to perform an action, it returns a structured tool call&lt;br&gt;&lt;br&gt;
instead of plain text which looks like JSON below:&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="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"tool_calls"&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;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"call_abc123"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"function"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"function"&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;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"lookup_product"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;name&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Aeropress Coffee Maker&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&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="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;Our code now needs to execute this function call and feed the result back which is all handled by RubyLLM in the background. The model then continues reasoning with the new information.&lt;/p&gt;

&lt;p&gt;This loop which &lt;em&gt;reasons, acts, and observes&lt;/em&gt; is what turns a language model into something that can actually get work done. And even better, we can wrap it all in a reusable agent class thanks to the RubyLLM Agents support. But first, let's implement the tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools
&lt;/h2&gt;

&lt;p&gt;RubyLLM tools are interfaces for runnable code we control in an LLM chat. Both &lt;code&gt;LookupProduct&lt;/code&gt; and &lt;code&gt;SearchGoogleShopping&lt;/code&gt; tools would be implemented as Ruby classes inherited from &lt;code&gt;RubyLLM::Tool&lt;/code&gt;. We name them using &lt;code&gt;description&lt;/code&gt;, provide a set of acceptable params using &lt;code&gt;param&lt;/code&gt;, and implement the &lt;code&gt;execute&lt;/code&gt; method.&lt;/p&gt;

&lt;p&gt;Here's an example of how this could look when searching a local product catalog for &lt;code&gt;LookupProduct&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;LookupProduct&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Tool&lt;/span&gt;
  &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="s2"&gt;"Looks up a product in our catalog by name"&lt;/span&gt;
  &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;desc: &lt;/span&gt;&lt;span class="s2"&gt;"Product name to search for"&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
     &lt;span class="no"&gt;Product&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"name ILIKE ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nb"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;%"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:sku&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:category&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="ss"&gt;:attributes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here's the code for &lt;code&gt;SearchGoogleShopping&lt;/code&gt; that works with Google Shopping using &lt;a href="https://serpapi.com" rel="noopener noreferrer"&gt;SerpApi&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SearchGoogleShopping&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Tool&lt;/span&gt;
  &lt;span class="n"&gt;description&lt;/span&gt; &lt;span class="s2"&gt;"Searches Google Shopping for current market prices of a product"&lt;/span&gt;
  &lt;span class="n"&gt;param&lt;/span&gt; &lt;span class="ss"&gt;:query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;desc: &lt;/span&gt;&lt;span class="s2"&gt;"The product to search for"&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;:)&lt;/span&gt;
    &lt;span class="n"&gt;search&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SerpApi&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;q: &lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;engine: &lt;/span&gt;&lt;span class="s2"&gt;"google_shopping"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;search&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;shopping_results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;first&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;title: &lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:title&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;price: &lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:price&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="ss"&gt;source: &lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:source&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;end&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can see that RubyLLM does all the heavy lifting, allowing us to write code as usual.&lt;/p&gt;

&lt;h2&gt;
  
  
  Agents
&lt;/h2&gt;

&lt;p&gt;Once we have our tools ready, we can introduce them to the chat using the &lt;code&gt;chat.with_tools&lt;/code&gt; call. We can also go one step further and wrap this up as a reusable class in an agent thanks to the &lt;code&gt;RubyLLM::Agent&lt;/code&gt; interface.&lt;/p&gt;

&lt;p&gt;An agent in this context is a class that couples instructions with a set of tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PriceMonitorAgent&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;RubyLLM&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Agent&lt;/span&gt;
  &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="s2"&gt;"gpt-5.4-mini"&lt;/span&gt;

  &lt;span class="n"&gt;instructions&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;~&lt;/span&gt;&lt;span class="no"&gt;PROMPT&lt;/span&gt;&lt;span class="sh"&gt;
      You are a pricing analyst. You help the merchandising team keep our product
      catalog competitively priced. You can look up our products, check current
      Google Shopping prices, and find products where we are significantly overpriced.
      Always show specific numbers when comparing prices.
&lt;/span&gt;&lt;span class="no"&gt;    PROMPT&lt;/span&gt;

  &lt;span class="n"&gt;tools&lt;/span&gt; &lt;span class="no"&gt;LookupProduct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;SearchGoogleShopping&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;FindUndercut&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;PriceMonitorAgent&lt;/code&gt; like the one above could help the shop's merchandising team find products that are overpriced on the current market:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;agent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;PriceMonitorAgent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;
&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ask&lt;/span&gt; &lt;span class="s2"&gt;"Which of our coffee products are priced more than 15% above market?"&lt;/span&gt;

&lt;span class="c1"&gt;# =&amp;gt; "I found 3 coffee products above the 15% threshold:&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# 1. Baratza Encore Grinder — Our price: $179.99, Market avg: $149.80 (20.2% over)&lt;/span&gt;
&lt;span class="c1"&gt;# 2. Fellow Stagg Kettle — Our price: $94.99, Market avg: $79.60 (19.3% over)&lt;/span&gt;
&lt;span class="c1"&gt;# 3. Chemex 6-Cup — Our price: $54.99, Market avg: $44.97 (22.3% over)&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# The Aeropress ($39.99 vs $37.48 avg) and Hario V60 ($11.99 vs $11.20 avg)&lt;/span&gt;
&lt;span class="c1"&gt;# are within range and look fine."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Depending on the question, the agent can pick one tool to generate a response or use an additional tool from the list to find the right answer. Combining internal data with live SERP data is incredibly powerful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Debugging
&lt;/h2&gt;

&lt;p&gt;Sometimes we might not be sure if the agents are using our tools in the way we expected. Luckily, chats in RubyLLM let us see all the tool calls that were done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;tool_calls&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;each&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="nb"&gt;puts&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;tc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;inspect&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we want to specifically recheck what search queries were run using SerpApi, we can head to &lt;a href="https://serpapi.com/searches" rel="noopener noreferrer"&gt;serpapi.com/searches&lt;/a&gt; after logging in, and find the searches that were done. We'll get a full &lt;strong&gt;Search Inspector&lt;/strong&gt; including the returned page and JSON:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4hadtmk1ypuyx89n03ec.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4hadtmk1ypuyx89n03ec.png" alt="SerpApi Search Inspector" width="800" height="452"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;SerpApi makes it super easy to integrate search data from &lt;a href="https://serpapi.com/search-api" rel="noopener noreferrer"&gt;Google&lt;/a&gt;, &lt;a href="https://serpapi.com/amazon-product-api" rel="noopener noreferrer"&gt;Amazon&lt;/a&gt;, &lt;a href="https://serpapi.com/bing-search-api" rel="noopener noreferrer"&gt;Bing&lt;/a&gt;, and other search engines into your application. And RubyLLM makes it easy to expose this API as a tool for your agents. Give SerpApi a try with 250 free searches/month.&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>rails</category>
      <category>aiagents</category>
    </item>
    <item>
      <title>Self-host SerpBear with Coolify</title>
      <dc:creator>Josef Strzibny</dc:creator>
      <pubDate>Thu, 02 Apr 2026 08:47:23 +0000</pubDate>
      <link>https://dev.to/serpapi/self-host-serpbear-with-coolify-569h</link>
      <guid>https://dev.to/serpapi/self-host-serpbear-with-coolify-569h</guid>
      <description>&lt;p&gt;Tracking organic positions on search engines is one of the main concerns of &lt;a href="https://serpapi.com/use-cases/seo" rel="noopener noreferrer"&gt;SEO&lt;/a&gt;. SerpBear is a SERP position tracking tool that will help you track how well you are doing in Google search. This post will take you through all the steps to start tracking your keywords with your own SerpBear instance on Coolify.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is SerpBear?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdn02fo1req3grn26po8q.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdn02fo1req3grn26po8q.png" alt="SerpBear logo" width="319" height="293"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/towfiqi/serpbear" rel="noopener noreferrer"&gt;SerpBear&lt;/a&gt; is an Open Source &lt;strong&gt;search engine position tracker&lt;/strong&gt;. It allows you to track your or your competitors' keyword positions in Google and to also get notified of these changes. Some of the features include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Unlimited keywords:&lt;/strong&gt;  You can add as many domains and keywords to track as you want. SerpBear will use a 3rd-party API such as SerpApi for actual tracking behind the scenes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexible scraping strategy:&lt;/strong&gt;  Choose your scraping strategy to control how many Google pages are checked for each domain. It's built to handle the Google's 2025 removal of the &lt;code&gt;num=100&lt;/code&gt;parameter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email notification:&lt;/strong&gt;  Get notified of all your keyword position changes via email with at the frequency of your choosing.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyword research:&lt;/strong&gt;  You have the option to do keyword research with the integration of Google Ads.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google Search Console:&lt;/strong&gt;  You can see the actual visit count and impressions for each keyword. Discover new keywords thanks to direct integration with Google Search Console.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exports:&lt;/strong&gt;  Export your domain keyword data in CSV files.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you want to track your keywords, but don't want to use SerpBear, have a look how to do your custom &lt;a href="https://serpapi.com/blog/serp-tracking-api-create-a-whiltelabel-rank-tracker-app/" rel="noopener noreferrer"&gt;SERP tracking in JavaScript&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Coolify?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://coolify.io" rel="noopener noreferrer"&gt;Coolify&lt;/a&gt; is an Open Source &lt;strong&gt;Platform as a Service&lt;/strong&gt; (PaaS) that will help you self-host SerpBear or any other self-hostable software out there. You can also deploy applications, databases, and one-click services.&lt;/p&gt;

&lt;p&gt;I am assuming you already have Coolify installed on your server. If you don't, you can learn how to &lt;a href="https://serpapi.com/blog/how-to-start-self-hosting-with-coolify-4-vps/" rel="noopener noreferrer"&gt;deploy Coolify on any VPS&lt;/a&gt; or take advantage of Coolify Cloud version where you don't have to host Coolify yourself.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡 We are going to deploy SerpBear 3.0 on Coolify 4.0. Coolify 4.0 is still beta software for the time being.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Signing up for SerpApi
&lt;/h2&gt;

&lt;p&gt;SerpApi is a service providing access to various SERP results with a simple API. SerpBear is free and open source software, but it uses SerpApi to do the heavy lifting of querying and parsing Google results. SerpBear cannot do this on its own due to the nature of managing proxies and solving CAPTCHAs, but you can still start with free searches to set everything up and see it working before committing to a paid plan.&lt;/p&gt;

&lt;p&gt;So first of all, &lt;a href="https://serpapi.com/users/sign_up" rel="noopener noreferrer"&gt;sign up for SerpApi&lt;/a&gt; and then note your private API key from &lt;a href="https://serpapi.com/manage-api-key" rel="noopener noreferrer"&gt;serpapi.com/manage-api-key&lt;/a&gt; page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7msjg3kgrsq42hzuuj5v.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F7msjg3kgrsq42hzuuj5v.png" alt="SerpApi dashboard" width="800" height="432"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;We are going to use this API key from SerpBear, but you can also use it to integrating one the many SerpApi SDKs to build anything needing &lt;a href="https://serpapi.com" rel="noopener noreferrer"&gt;SERP API&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring DNS
&lt;/h2&gt;

&lt;p&gt;Head over to your domain registrar console and set the DNS A records for domains or subdomains you want to use for SerpBear:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;coolify.example.com -&amp;gt; 178.104.25.112
serpbear.example.com -&amp;gt; 178.104.25.112
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Choose "A" type records to directly connect a domain name or subdomain to the IP address (replace the values, the above is just an example). If you'll run SerpBear on the same host as Coolify, the IP address remains the same for both.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding SerpBear
&lt;/h2&gt;

&lt;p&gt;If your Coolify instance is fully set up, it's time to add SerpBear. To run a new Coolify project head over to &lt;em&gt;Projects&lt;/em&gt; (from the left menu) and click on &lt;strong&gt;+ Add&lt;/strong&gt; next to the Projects headline. Give it a name and description.&lt;/p&gt;

&lt;p&gt;From Projects, click on &lt;strong&gt;+ Add Resource&lt;/strong&gt; next to your project name or select your project first and click on &lt;strong&gt;+ New&lt;/strong&gt; next to &lt;em&gt;Resources&lt;/em&gt;. From here, we can select &lt;strong&gt;Docker Compose Empty&lt;/strong&gt; under &lt;em&gt;Docker Based&lt;/em&gt; on the right:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fimetq2p3oc3qu5bp4if5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fimetq2p3oc3qu5bp4if5.png" alt="Coolify's New Resource page" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SerpBear provides Docker Compose configuration which Coolify supports. Since SerpBear uses SQLite to save SERP data, it's not necessary to spin up process-based databases and the Compose file is relatively simple:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;services:
  app:
    image: towfiqi/serpbear:latest
    # Build from source instead of pulling the image:
    # build: .
    restart: unless-stopped
    ports:
      - "${PORT:-3000}:3000"
    environment:
      - USER_NAME=${USER:-admin}
      - PASSWORD=${PASSWORD}
      - SECRET=${SECRET}
      - APIKEY=${APIKEY}
      - SESSION_DURATION=${SESSION_DURATION:-24}
      - NEXT_PUBLIC_APP_URL=${NEXT_PUBLIC_APP_URL:-http://localhost:3000}
      # Optional: Google Search Console integration
      - SEARCH_CONSOLE_CLIENT_EMAIL=${SEARCH_CONSOLE_CLIENT_EMAIL:-}
      - SEARCH_CONSOLE_PRIVATE_KEY=${SEARCH_CONSOLE_PRIVATE_KEY:-}
    volumes:
      - serpbear_data:/app/data
    healthcheck:
      test: ["CMD-SHELL", "wget -qO- http://localhost:3000 || exit 1"]
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 15s

volumes:
  serpbear_data:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The SerpBear service definition includes the official Docker image, container restart policy, ports, environment variables, volumes, and health check. The official guide suggests using the latest image (&lt;code&gt;towfiqi/serpbear:latest&lt;/code&gt;) which will let you update the application with &lt;strong&gt;Redeploy&lt;/strong&gt; at any later point, but you can choose a stable release to lock-in a particular version.&lt;/p&gt;

&lt;p&gt;The required environment variables reference variables like &lt;code&gt;{USER:-admin}&lt;/code&gt; and &lt;code&gt;{SECRET}&lt;/code&gt; which let us manage the environment directly from the Coolify admin interface. Notice that SerpBear also supports Google Search Console, so if you can obtain these credentials as well. Finally notice the &lt;code&gt;serpbear_data&lt;/code&gt; volume which is the future location of your SerpBear database.&lt;/p&gt;

&lt;p&gt;Paste the configuration above. Click &lt;strong&gt;Save&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Adding the new resource should take you to the resource configuration. Under &lt;em&gt;General&lt;/em&gt; we can change the resource name, under &lt;em&gt;Persistent Storages&lt;/em&gt; we can check our persistent volume, and finally under &lt;em&gt;Environment Variables&lt;/em&gt; we can go fill up the variables from the Compose file:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4jygvw1jk4gh62y1hksp.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4jygvw1jk4gh62y1hksp.png" alt="Coolify configuration" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Here's the list of environment variables we should fill in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;USER_NAME&lt;/code&gt;: The username you want to use to login to the app.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;PASSWORD&lt;/code&gt;: The password you want to use to log in to the app.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SECRET&lt;/code&gt;: A secret key that will be used for encrypting 3rd party API keys &amp;amp; passwords.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;APIKEY&lt;/code&gt;: API key that will be used to access the app's API. This is not SerpApi account key!&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SESSION_DURATION&lt;/code&gt;: The duration(in hours) of the user's logged-in session.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NEXT_PUBLIC_APP_URL&lt;/code&gt;: The URL where your app is hosted and can be accessed like &lt;a href="https://serpbear.example.com" rel="noopener noreferrer"&gt;https://serpbear.example.com&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note that every environment entry has its own &lt;strong&gt;Update&lt;/strong&gt; button and they won't be saved all at once. You can add the Google Search Console credentials if you have them, but they aren't necessary to start tracking your keywords with SerpApi.&lt;/p&gt;

&lt;p&gt;Restart the service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tracking keywords with SerpBear
&lt;/h2&gt;

&lt;p&gt;If you carefully followed all the steps, you should be now able to access the SerpBear instance on the (sub)domain of your choosing. Log in using the chosen credentials from the previous step. You should see a red prompt at the top to configure your &lt;em&gt;Scrapper/Proxy&lt;/em&gt;. Click on it to open settings, choose &lt;em&gt;SerpApi.com&lt;/em&gt; as the &lt;em&gt;Scraping Method&lt;/em&gt; and insert your SerpApi API token under &lt;em&gt;Scraper API Key Or Token&lt;/em&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fixend0ftf2mskym40yxi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fixend0ftf2mskym40yxi.png" alt="SerpBear settings" width="800" height="551"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Update Settings&lt;/strong&gt; to close the right sidebar and you'll be ready to add your first keywords to track. Start by adding a domain name. After entering your domain name you should end up on the &lt;em&gt;Tracking&lt;/em&gt; tab for the domain.&lt;/p&gt;

&lt;p&gt;Click &lt;strong&gt;Add Keyword&lt;/strong&gt; and start adding keywords and phrases you care about. They should appear in a nice table with their last position, best position, history graph, volume, and URL. Here's an example of tracking "serp api" for serpapi.com in the Czech Republic:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frm9j1lmysprto8m5z0pi.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Frm9j1lmysprto8m5z0pi.png" alt="SerpBear keywords" width="800" height="551"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And that's it! You can now add as many domains and keywords as you want to. Remember that SEO today is not just about tracking organic results in Google, so have a look at &lt;a href="https://serpapi.com/blog/rank-tracking-in-the-age-of-ai-overviews-whats-changed/" rel="noopener noreferrer"&gt;AI overviews&lt;/a&gt; as well.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;As next steps you can consider adding your Google Search Console data to SerpBear, handle external backups in Coolify, or exploring more of &lt;a href="https://serpapi.com" rel="noopener noreferrer"&gt;SerpApi&lt;/a&gt;. The Free tier comes with &lt;strong&gt;250 free searches&lt;/strong&gt; per month.&lt;/p&gt;

</description>
      <category>selfhosting</category>
    </item>
    <item>
      <title>How to start self-hosting with Coolify 4 on a VPS</title>
      <dc:creator>Josef Strzibny</dc:creator>
      <pubDate>Wed, 01 Apr 2026 11:57:51 +0000</pubDate>
      <link>https://dev.to/serpapi/how-to-start-self-hosting-with-coolify-4-on-a-vps-44ob</link>
      <guid>https://dev.to/serpapi/how-to-start-self-hosting-with-coolify-4-on-a-vps-44ob</guid>
      <description>&lt;p&gt;Coolify is an open source Platform as a Service (PaaS) that can help you self-host a lot of different software from content management tools like Ghost to SEO tracking software like SerpBear. The only thing you'll need is a virtual private server (VPS) from a hosting provider of your choice and a little bit of set up which is exactly what we'll go through in this post.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡&lt;/p&gt;

&lt;p&gt;Coolify v4 is still beta software as of the time of writing of this post. If you need a little bit more stability, wait for the final release.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What is Coolify?
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvv4bflbq7tzhjagp3lv0.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fvv4bflbq7tzhjagp3lv0.png" alt="Coolify logo" width="800" height="455"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/coollabsio/coolify" rel="noopener noreferrer"&gt;Coolify&lt;/a&gt; is a self-hostable alternative to Heroku, Vercel, or Netlify. It's an open source PaaS that allows developers to deploy their applications as well as manage 3rd-party services and databases.&lt;/p&gt;

&lt;p&gt;Some of the biggest advantages include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted control:&lt;/strong&gt;  You own your data on your own servers which saves money and avoids vendor lock-in.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting versatility:&lt;/strong&gt;  Hosts web applications, static websites, databases (PostgreSQL, MySQL, MongoDB, etc.), and services&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automated DevOps:&lt;/strong&gt;  Automatically installs dependencies, sets up databases, and handles deployments from GitHub, GitLab, or Bitbucket.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web console:&lt;/strong&gt;  Offers a web-based dashboard for managing multiple servers, viewing logs, and monitoring resource usage like CPU and RAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Built-in tools:&lt;/strong&gt;  Features automatic S3-compatible backups, auto provisioning of SSL certificates, webhooks, or preview deployments.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can run Coolify in various ways. You can use a single server for everything you host or do a separate deployment for the services you'll run.&lt;/p&gt;

&lt;h3&gt;
  
  
  Applications, databases and services
&lt;/h3&gt;

&lt;p&gt;Coolify supports deploying custom applications and 3rd-party services. It generally distinguish between 3 different types of resources:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Applications&lt;/strong&gt; come with &lt;strong&gt;a git source&lt;/strong&gt; or are built using Docker from &lt;strong&gt;Dockerfile&lt;/strong&gt;. You can deploy anything from &lt;a href="https://serpapi.com/integrations/php" rel="noopener noreferrer"&gt;PHP&lt;/a&gt; to &lt;a href="https://serpapi.com/integrations/java" rel="noopener noreferrer"&gt;Java&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Databases&lt;/strong&gt; are preconfigured Docker images for running databases, like MySQL or PostgreSQL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Services&lt;/strong&gt; are deployments based on  &lt;strong&gt;Docker Compose&lt;/strong&gt; files that are stored directly on the server. You deploy well known software like Ghost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can mix and match any of these on a single Coolify instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setting up the virtual server
&lt;/h2&gt;

&lt;p&gt;There are two variants to self-hosting. One with physical servers that are usually dedicated to you and one with virtual servers where you share the underlying physical servers with others.&lt;/p&gt;

&lt;p&gt;We'll set up a simple virtual server on Hetzner which is a provider praised for its affordable costs. You are free to use any other provider you like or already have. Some other popular providers include Digital Ocean, Vultr, or OVHcloud.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡&lt;/p&gt;

&lt;p&gt;Coolify currently offers $20 credit for deploying to Hetzner by following the &lt;a href="https://coolify.io/hetzner" rel="noopener noreferrer"&gt;https://coolify.io/hetzner&lt;/a&gt; link.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To create a virtual private server (VPS), you'll need an SSH key pair which you'll authenticate with in the future. You can optionally set a password for the private key as well. To generate a new one, run &lt;code&gt;ssh-keygen&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh-keygen
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;The command will interactively ask you about your key details. If you name it &lt;em&gt;coolify&lt;/em&gt; you should end up with two files inside the &lt;code&gt;~/.ssh&lt;/code&gt; directory, one for private key and one for public key (&lt;code&gt;coolify.pub&lt;/code&gt;).&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;💡&lt;/p&gt;

&lt;p&gt;Protect your SSH private key! You always provide others with your public key, not the private key. The private key should stay with you on your computer.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Once you have your key pair, it's fairly straightforward to spin up a new server with the provider of your choice.&lt;/p&gt;

&lt;p&gt;On Hetzner, create a new project and on the project overview click &lt;strong&gt;Add Server&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffgr3ye1zo0zmrcobmmg6.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Ffgr3ye1zo0zmrcobmmg6.png" alt="Creating a server on Hetzner" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Select a location close to you, Ubuntu 24.04 or your favorite operating system (Debian, SUSE, and Fedora based systems are supported), the size you want, and optionally enable full virtual machine backups. Under &lt;em&gt;SSH Keys&lt;/em&gt; upload your &lt;strong&gt;public SSH key&lt;/strong&gt; which should disable password-based access on most providers and let you use your private key instead.&lt;/p&gt;

&lt;p&gt;As for the size of the box, Coolify recommends at least 2 vCPUs, 2 GB of RAM, and 30 GB of storage space. If you are going to self-host projects on the same instance, you generally need to increase the box size accordingly. For only running one or two extra applications, consider increasing RAM.&lt;/p&gt;

&lt;p&gt;This is all that's needed for things to work, but we can also provide a custom cloud-init script under &lt;em&gt;Cloud config&lt;/em&gt;. For example, we can set up automatic system updates and install fail2ban for extra protection of our SSH port:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Do a system update
apt update;
DEBIAN_FRONTEND=noninteractive apt upgrade -y

# Install essential packages
apt install -y curl unattended-upgrades fail2ban

# Set up unattented updates
echo -e "APT::Periodic::Update-Package-Lists \"1\";\nAPT::Periodic::Unattended-Upgrade \"1\";\n" &amp;gt; /etc/apt/apt.conf.d/20auto-upgrades
/etc/init.d/unattended-upgrades restart

# Install fail2ban
systemctl start fail2ban.service
systemctl enable fail2ban.service
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You can optionally install Docker at this step from the system package manager but &lt;strong&gt;do not&lt;/strong&gt; install Docker from Snap as that's not supported by Coolify.&lt;/p&gt;

&lt;p&gt;Finally, give the server a name (you can call it &lt;em&gt;coolify&lt;/em&gt;, but do not use a domain name) and click &lt;strong&gt;Create &amp;amp; Buy now&lt;/strong&gt;. Provisioning the server will take some minutes and once done you should be able to find the public server IPv4 address next to its name on the server page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgwrli5gt5me89w7jtem5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fgwrli5gt5me89w7jtem5.png" alt="Server details on Hetzner" width="800" height="478"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should now be able to log in from your terminal as:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh-add ~/.ssh/path-to-your-key
ssh root@[IP_ADDRESS]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;There is quite a bit more to running and securing servers. You should ideally choose a different user than &lt;em&gt;root&lt;/em&gt; and/or disable public SSH access altogether in the cloud's firewall. If you want to do that, have a look at WireGuard or Tailscale.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring DNS
&lt;/h2&gt;

&lt;p&gt;Now that you have a public IP address, you can create DNS records to get nice URLs for accessing Coolify, SerpBear, and whatever else you decide to self host.&lt;/p&gt;

&lt;p&gt;Head over to your domain registrar console and set the DNS A records for domains or subdomains you want to use for Coolify and the applications you want to host:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# For accesing Coolify web console (admin area)
coolify.example.com -&amp;gt; 178.104.25.112

# Example application
serpbear.example.com -&amp;gt; 178.104.25.112
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Choose "A" type records to directly connect a domain name or subdomain to the IP address (replace the values, the above is just an example). You can use Porkbun or GoDaddy to register your first domain name if you don't have one yet.&lt;/p&gt;

&lt;p&gt;Since we'll run everything on a single host, the IP address remains the same for all instances.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Coolify
&lt;/h2&gt;

&lt;p&gt;Now that we have our Linux box up and running, we can SSH into it and run the official installer script:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -fsSL https://cdn.coollabs.io/coolify/install.sh | sudo bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;You can change some installation details with environment variables, so go and &lt;a href="https://coolify.io/docs/get-started/installation#advanced-customizing-installation-with-environment-variables" rel="noopener noreferrer"&gt;review this list&lt;/a&gt; before running the script.&lt;/p&gt;

&lt;p&gt;For example, you might want to choose the admin username, email, and password:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;env ROOT_USERNAME=admin \
ROOT_USER_EMAIL=admin@example.com \
ROOT_USER_PASSWORD=SecurePassword123 \
bash -c 'curl -fsSL https://cdn.coollabs.io/coolify/install.sh | bash'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Once run, you'll see a &lt;em&gt;Congratulations!&lt;/em&gt; screen with the list of the IP addresses, version as well as a log file:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;____ _ _ _ _ _
  / ___|___ _ ____ _ _ ____ _| |_ _ _| | ___| |_(_)___ _ _____ | |
 | | / _ \| '_ \ / _` | ' __/ _` |__ | | | | |/ _` | __| |/ _ \| '_ \/__ | |
 | |__| (_) | | | | (_| | | | (_| | |_| |_| | | (_| | |_| | (_) | | | \__ \_|
  \ ____\___ /|_| |_|\ __, |_| \__ ,_|\ __|\__ ,_|_|\ __,_|\__ |_|\ ___/|_| |_|___ (_)
                   |___/


Your instance is ready to use!

You can access Coolify through your Public IPV4: http://178.104.25.112:8000
You can access Coolify through your Public IPv6: http://[2a01:4f8:1c19:f81a::1]:8000

If your Public IP is not accessible, you can use the following Private IPs:

http://10.0.0.1:8000
http://10.0.1.1:8000
http://2a01:4f8:1c19:f81a::1:8000
http://fdb0:c330:3639::1:8000

WARNING: It is highly recommended to backup your Environment variables file (/data/coolify/source/.env) to a safe location, outside of this server (e.g. into a Password Manager).


============================================================
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;[2026-03-19 11:02:44] Installation Complete&lt;br&gt;
    ============================================================&lt;br&gt;
[2026-03-19 11:02:44] Coolify installation completed successfully&lt;br&gt;
[2026-03-19 11:02:44] Version: 4.0.0-beta.468&lt;br&gt;
[2026-03-19 11:02:44] Log file: /data/coolify/source/installation-20260319-110120.log&lt;/p&gt;

&lt;p&gt;After installation, you should find your self-hosted Coolify instance at &lt;a href="http://203.0.113.1:8000" rel="noopener noreferrer"&gt;&lt;code&gt;http://[IP_ADDRESS]:8000&lt;/code&gt;&lt;/a&gt;:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fosc405o9brtdo9030uvq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fosc405o9brtdo9030uvq.png" alt="Coolify welcome page" width="800" height="368"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You should see a &lt;em&gt;Welcome to Coolify&lt;/em&gt; screen. Click &lt;strong&gt;Let's go!&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6veytuocsa5v3x7zabat.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F6veytuocsa5v3x7zabat.png" alt="Coolify sign up" width="800" height="398"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create your root account and on the next screen choose &lt;em&gt;This Machine&lt;/em&gt; as your server type (we'll run everything on a single instance):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ctgo1o5skz5ogwxie3m.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2ctgo1o5skz5ogwxie3m.png" alt="Coolify server type selection" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After the initial wizard, let's go to &lt;em&gt;Settings&lt;/em&gt; (from the left menu) and input the instance URL with our (sub)domain we prepared for Coolify under &lt;em&gt;General&lt;/em&gt; (make sure to include full URL including the leading &lt;code&gt;https://&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2un11h72yuweql3dscb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fv2un11h72yuweql3dscb.png" alt="Coolify localhost instance settings" width="800" height="556"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Optionally you can also explore the &lt;em&gt;Backup&lt;/em&gt; and &lt;em&gt;Transactional Email&lt;/em&gt; tabs for setting up Coolify backups and transactional emails (for things like forgotten password).&lt;/p&gt;

&lt;p&gt;Then head over to &lt;em&gt;Servers&lt;/em&gt; (from the left menu) where you should see our &lt;em&gt;localhost&lt;/em&gt; instance (running Coolify). Here you'll find settings for the Coolify components. Open it and go to the &lt;em&gt;Proxy&lt;/em&gt; tab:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhgq3495fzrngc5j129jc.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhgq3495fzrngc5j129jc.png" alt="Coolify proxy" width="800" height="479"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Checking that the proxy is running is important as it will route the traffic to the applications you'll self host. Even if Coolify is running and you are logged in inside the console, the &lt;em&gt;coolify-proxy&lt;/em&gt; might not be.&lt;/p&gt;

&lt;p&gt;In case the proxy is not running, open the logs and see what's wrong. In my case, the proxy wasn't running and I found the following inside the logs:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Creating required Docker Compose file.
Pulling docker image.
 Image traefik:v3.6 Pulling 
 Image traefik:v3.6 Pulled 
Ensuring network coolify exists...
Ensuring network havzqyfu212ybs9n5lg48gzy exists...
Starting coolify-proxy.
ParseAddr("fdb0:c330:3639::1/64"): unexpected character, want colon (at "/64")
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;There seemed to be an issue with parsing an IPv6 address. To resolve it I decided to turn off IPv6 support on the system. I opened an SSH connection to the server again to edit Docker's daemon.json file:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vi /etc/docker/daemon.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;I added an entry with &lt;code&gt;"ipv6": false&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "10m",
    "max-file": "3"
  },
  "default-address-pools": [
    {"base":"10.0.0.0/8","size":24}
  ],
  "ipv6": false
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Then I restarted Docker:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;systemctl restart docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;After that I could restart the proxy and see coolify-proxy container running on the system.&lt;/p&gt;

&lt;p&gt;And that was it! If you can log in and see that the proxy is running, you are ready to start adding individual applications and services. You can still also use a hosted version of Coolify if you never set up a server before and all of this looks daunting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deploying resources
&lt;/h2&gt;

&lt;p&gt;Resources in Coolify needs a project, so open &lt;em&gt;Projects&lt;/em&gt; from the left side menu, and click &lt;strong&gt;+ Add&lt;/strong&gt; next to Projects. Then click &lt;strong&gt;+ New&lt;/strong&gt; next to the &lt;em&gt;Resources&lt;/em&gt; headline. You should arrive on a &lt;em&gt;New Resource&lt;/em&gt; page which lets you choose what you want to run. You can deploy directly from a git repository, Dockerfile or custom Docker Compose.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlk8fn21xy2pa5hi5xll.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fhlk8fn21xy2pa5hi5xll.png" alt="Coolify applications" width="800" height="458"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  One-click services
&lt;/h3&gt;

&lt;p&gt;One-click services are pre-configured Docker Compose templates provided directly by Coolify, skipping the complexity of manual setup and configuration. They are the easiest to start with.&lt;/p&gt;

&lt;p&gt;Scroll a bit down and you should see them:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ffb26wsxrw9r0a9pgvv.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9ffb26wsxrw9r0a9pgvv.png" alt="Coolify one-click services" width="800" height="375"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Coolify also maintains &lt;a href="https://coolify.io/docs/services/all" rel="noopener noreferrer"&gt;a directory&lt;/a&gt; for these services on the web. Some of services you can add are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Internal tools:&lt;/strong&gt;  Appsmith, Budibase, NocoDB.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chat &amp;amp; messaging:&lt;/strong&gt;  Matrix, Mattermost, Rocket.Chat.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Development &amp;amp; CI/CD:&lt;/strong&gt;  VS Code Server, Gitea, Jenkins.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Monitoring &amp;amp; analytics:&lt;/strong&gt;  Bugsink, CloudBeaver.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CMS:&lt;/strong&gt;  WordPress, Ghost.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They are all pretested, optimized, and coming with sensible defaults.&lt;/p&gt;

&lt;h3&gt;
  
  
  Custom services
&lt;/h3&gt;

&lt;p&gt;Custom services in Coolify are those that you deploy with your own &lt;strong&gt;Docker Compose&lt;/strong&gt; file. They require a little bit more work, but allow you to run almost anything. Have a look at &lt;a href="https://serpapi.com/blog/self-host-serpbear-coolify/" rel="noopener noreferrer"&gt;how to deploy SerpBear&lt;/a&gt;, a SEO tool for keyword tracking as an example of a custom service.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next steps
&lt;/h2&gt;

&lt;p&gt;If you are new to self-hosting make sure to learn a little bit more about SSH, DNS, Linux, Docker, and related topics. You should also have a look at the Coolify &lt;a href="https://coolify.io/docs/" rel="noopener noreferrer"&gt;documentation&lt;/a&gt; for even more information and tips.&lt;/p&gt;

</description>
      <category>selfhosting</category>
    </item>
    <item>
      <title>I wrote a handbook for Kamal</title>
      <dc:creator>Josef Strzibny</dc:creator>
      <pubDate>Fri, 05 Apr 2024 08:49:28 +0000</pubDate>
      <link>https://dev.to/strzibny/i-wrote-a-handbook-for-kamal-1d76</link>
      <guid>https://dev.to/strzibny/i-wrote-a-handbook-for-kamal-1d76</guid>
      <description>&lt;p&gt;Kamal is an imperative deployment tool. It's basically a successor to Capistrano, but for a container era. It's a simple wrapper around Docker and that's the whole beauty of it. 37signals created Kamal to self-host Basecamp and Hey as part of their pull out of the cloud (running managed K8s).&lt;/p&gt;

&lt;p&gt;I am always for simplicity when it comes to deployment and the truth is that lots of projects out there don't need the fully-featured Kubernetes to run. When Kamal was released I got intrigued and slowly adopted the tool. Nowadays I deploy all new projects with it.&lt;/p&gt;

&lt;p&gt;Since the documentation is a little sparse at the moment and some people trying Kamal abandoned the effort when they faced their first issues, I decided to do something about it and wrote 'the missing manual' to Kamal called &lt;a href="https://kamalmanual.com/handbook/"&gt;Kamal Handbook&lt;/a&gt;. &lt;/p&gt;

&lt;p&gt;The book goes through all the important aspects of deploying with Kamal, describes the design choices and explains what's happening under the hood with illustrations. It's by design a small book you can read in a weekend. Hope you'll like it.&lt;/p&gt;

</description>
      <category>linux</category>
      <category>docker</category>
      <category>kamal</category>
    </item>
    <item>
      <title>I just released InvoicePrinter 2.0</title>
      <dc:creator>Josef Strzibny</dc:creator>
      <pubDate>Wed, 02 Oct 2019 11:50:08 +0000</pubDate>
      <link>https://dev.to/strzibny/i-just-released-invoiceprinter-2-0-5da0</link>
      <guid>https://dev.to/strzibny/i-just-released-invoiceprinter-2-0-5da0</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/strzibny/invoice_printer"&gt;InvoicePrinter&lt;/a&gt; is a Ruby library, server, and command-line client to make beautiful PDF invoices without browsers in no time. It's pure Ruby, but can be also used outside the Ruby world as a Docker container with a simple JSON API.&lt;/p&gt;

&lt;p&gt;New features in 2.0:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;New modern look &amp;amp; feel&lt;/li&gt;
&lt;li&gt;More flexible buyer/seller boxes&lt;/li&gt;
&lt;li&gt;Server is decoupled to a separate gem &lt;code&gt;invoice_printer_server&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;New bundled fonts in a separate gem &lt;code&gt;invoice_printer_fonts&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Official Docker image&lt;/li&gt;
&lt;li&gt;Prawn 2.2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Here is the project &lt;a href="https://github.com/strzibny/invoice_printer"&gt;GitHub page&lt;/a&gt;, in the &lt;code&gt;examples/&lt;/code&gt; directory you can find a lot of code examples and how they look as PDFs. Follow &lt;code&gt;docs/&lt;/code&gt; to learn more about using it as a library, server or a command-line client.&lt;/p&gt;

&lt;p&gt;Here is one example (&lt;a href="https://github.com/strzibny/invoice_printer/raw/master/examples/promo_a4.pdf"&gt;download the final PDF&lt;/a&gt;):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YoDO3fMc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/5ro56bflouziq6puy2kx.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YoDO3fMc--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://thepracticaldev.s3.amazonaws.com/i/5ro56bflouziq6puy2kx.jpg" alt="Alt Text" width="800" height="1132"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And Ruby code to generate it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env ruby&lt;/span&gt;
&lt;span class="c1"&gt;# This is an example of a international invoice with Czech labels and English translation.&lt;/span&gt;

&lt;span class="nb"&gt;require&lt;/span&gt; &lt;span class="s1"&gt;'invoice_printer'&lt;/span&gt;

&lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'Faktura'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;provider: &lt;/span&gt;&lt;span class="s1"&gt;'Prodejce'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;purchaser: &lt;/span&gt;&lt;span class="s1"&gt;'Kupující'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;tax_id: &lt;/span&gt;&lt;span class="s1"&gt;'IČ'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;tax_id2: &lt;/span&gt;&lt;span class="s1"&gt;'DIČ'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;payment: &lt;/span&gt;&lt;span class="s1"&gt;'Forma úhrady'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;payment_by_transfer: &lt;/span&gt;&lt;span class="s1"&gt;'Platba na následující účet:'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;account_number: &lt;/span&gt;&lt;span class="s1"&gt;'Číslo účtu'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;issue_date: &lt;/span&gt;&lt;span class="s1"&gt;'Datum vydání'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;due_date: &lt;/span&gt;&lt;span class="s1"&gt;'Datum splatnosti'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;item: &lt;/span&gt;&lt;span class="s1"&gt;'Položka'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;quantity: &lt;/span&gt;&lt;span class="s1"&gt;'Počet'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;unit: &lt;/span&gt;&lt;span class="s1"&gt;'MJ'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;price_per_item: &lt;/span&gt;&lt;span class="s1"&gt;'Cena za položku'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;amount: &lt;/span&gt;&lt;span class="s1"&gt;'Celkem bez daně'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;subtotal: &lt;/span&gt;&lt;span class="s1"&gt;'Cena bez daně'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;tax: &lt;/span&gt;&lt;span class="s1"&gt;'DPH 21 %'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;total: &lt;/span&gt;&lt;span class="s1"&gt;'Celkem'&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Default English labels as sublabels&lt;/span&gt;
&lt;span class="n"&gt;sublabels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;InvoicePrinter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;PDFDocument&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;DEFAULT_LABELS&lt;/span&gt;
&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;merge!&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="ss"&gt;sublabels: &lt;/span&gt;&lt;span class="n"&gt;sublabels&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;first_item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;InvoicePrinter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'Konzultace'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;quantity: &lt;/span&gt;&lt;span class="s1"&gt;'2'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;unit: &lt;/span&gt;&lt;span class="s1"&gt;'hod'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;price: &lt;/span&gt;&lt;span class="s1"&gt;'Kč 500'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;amount: &lt;/span&gt;&lt;span class="s1"&gt;'Kč 1.000'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;second_item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;InvoicePrinter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Document&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s1"&gt;'Programování'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;quantity: &lt;/span&gt;&lt;span class="s1"&gt;'10'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;unit: &lt;/span&gt;&lt;span class="s1"&gt;'hod'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;price: &lt;/span&gt;&lt;span class="s1"&gt;'Kč 900'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;amount: &lt;/span&gt;&lt;span class="s1"&gt;'Kč 9.000'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;provider_address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;ADDRESS&lt;/span&gt;&lt;span class="sh"&gt;
Rolnická 1
747 05  Opava
Kateřinky
&lt;/span&gt;&lt;span class="no"&gt;ADDRESS&lt;/span&gt;

&lt;span class="n"&gt;purchaser_address&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="no"&gt;ADDRESS&lt;/span&gt;&lt;span class="sh"&gt;
Ostravská 1
747 70  Opava
&lt;/span&gt;&lt;span class="no"&gt;ADDRESS&lt;/span&gt;

&lt;span class="n"&gt;invoice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;InvoicePrinter&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;number: &lt;/span&gt;&lt;span class="s1"&gt;'č. 198900000001'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;provider_name: &lt;/span&gt;&lt;span class="s1"&gt;'Petr Nový'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;provider_lines:  &lt;/span&gt;&lt;span class="n"&gt;provider_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;provider_tax_id: &lt;/span&gt;&lt;span class="s1"&gt;'56565656'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;purchaser_name: &lt;/span&gt;&lt;span class="s1"&gt;'Adam Černý'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;purchaser_lines: &lt;/span&gt;&lt;span class="n"&gt;purchaser_address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;issue_date: &lt;/span&gt;&lt;span class="s1"&gt;'05/03/2016'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;due_date: &lt;/span&gt;&lt;span class="s1"&gt;'19/03/2016'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;subtotal: &lt;/span&gt;&lt;span class="s1"&gt;'Kč 10.000'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;tax: &lt;/span&gt;&lt;span class="s1"&gt;'Kč 2.100'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;total: &lt;/span&gt;&lt;span class="s1"&gt;'Kč 12.100,-'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;bank_account_number: &lt;/span&gt;&lt;span class="s1"&gt;'156546546465'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;account_iban: &lt;/span&gt;&lt;span class="s1"&gt;'IBAN464545645'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;account_swift: &lt;/span&gt;&lt;span class="s1"&gt;'SWIFT5456'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;items: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;first_item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;second_item&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="ss"&gt;note: &lt;/span&gt;&lt;span class="s1"&gt;'Osoba je zapsána v živnostenském rejstříku.'&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="no"&gt;InvoicePrinter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="ss"&gt;document: &lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;labels: &lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;font: &lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'../../assets/fonts/overpass/Overpass-Regular.ttf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;__FILE__&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="ss"&gt;logo: &lt;/span&gt;&lt;span class="no"&gt;File&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;expand_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'../logo.png'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kp"&gt;__FILE__&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="ss"&gt;file_name: &lt;/span&gt;&lt;span class="s1"&gt;'promo.pdf'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="ss"&gt;page_size: :a4&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before the final release I wrote a little background post on my &lt;a href="http://nts.strzibny.name/invoiceprinter-2-0/"&gt;blog&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Try it out and let me know what you think!&lt;/p&gt;

</description>
      <category>ruby</category>
      <category>docker</category>
      <category>pdf</category>
      <category>invoicing</category>
    </item>
    <item>
      <title>Django 2.2 polls app tutorial source code commit by commit</title>
      <dc:creator>Josef Strzibny</dc:creator>
      <pubDate>Tue, 09 Apr 2019 22:47:01 +0000</pubDate>
      <link>https://dev.to/strzibny/django-2-2-polls-app-tutorial-source-code-commit-by-commit-3glk</link>
      <guid>https://dev.to/strzibny/django-2-2-polls-app-tutorial-source-code-commit-by-commit-3glk</guid>
      <description>&lt;p&gt;TL;DR&lt;/p&gt;

&lt;p&gt;I went through the famous Django "polls" app tutorial, and made it into a git repository which you can follow commit by commit:&lt;/p&gt;

&lt;p&gt;GitHub link: &lt;a href="https://github.com/deployment-from-scratch/django-2.2-polls"&gt;deployment-from-scratch/django-2.2-polls&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Tutorial link: &lt;a href="https://docs.djangoproject.com/en/2.2/intro/tutorial01/"&gt;Writing your first Django app&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;It's for Django 2.2.&lt;/p&gt;

&lt;p&gt;Full Story:&lt;/p&gt;

&lt;p&gt;Since the beginning of time (me wanting to leave PHP development) I was intrigued by Ruby on Rails and Django frameworks. I liked both for different reasons, but ended up doing mainly Rails because of Ruby the language (and living with a syndrome that I might made a mistake ever since).&lt;/p&gt;

&lt;p&gt;Fast forward to 2019, and I am writing a book on &lt;a href="http://deploymentfromscratch.com"&gt;deploying web application&lt;/a&gt; where I am trying to teach deployment of both Ruby &lt;em&gt;and&lt;/em&gt; Python apps. There are pretty similar in that regards actually, but I needed some Python examples.&lt;/p&gt;

&lt;p&gt;For this reason, I went to the Django site after many years (it's a lie, I keep watching Django space haha) and did the famous introductory tutorial of building "polls" app. I did it "commit by commit" so people can follow it easily while going through the tutorial.&lt;/p&gt;

</description>
      <category>python</category>
      <category>django</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
