<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>Other Things</title>
	<atom:link href="https://blog.adamzolo.com/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.adamzolo.com</link>
	<description>Blog about Things by Adam Zolotarev</description>
	<lastBuildDate>Sat, 09 May 2026 14:00:17 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>
	<item>
		<title>Using tools for LLM&#8217;s instead of asking</title>
		<link>https://blog.adamzolo.com/claude-api-tool-use-vs-prompting/</link>
					<comments>https://blog.adamzolo.com/claude-api-tool-use-vs-prompting/#respond</comments>
		
		<dc:creator><![CDATA[Adam Zolo]]></dc:creator>
		<pubDate>Tue, 17 Feb 2026 03:26:32 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[Rails]]></category>
		<category><![CDATA[Ruby]]></category>
		<guid isPermaLink="false">https://blog.adamzolo.com/?p=1106</guid>

					<description><![CDATA[I have a Rails app that sends user-provided text to Claude for analysis and displays structured results in the UI. The response needs to be JSON so I can render it. However longer inputs sometimes would generate errors. Longer inputs meant longer system prompts and longer responses. The logs showed: Analysis failed: expected ',' or&#8230;<p><a class="more-link" href="https://blog.adamzolo.com/claude-api-tool-use-vs-prompting/" title="Continue reading &#8216;Using tools for LLM&#8217;s instead of asking&#8217;">Continue reading <span class="meta-nav">&#8594;</span></a></p>]]></description>
										<content:encoded><![CDATA[<p>I have a Rails app that sends user-provided text to Claude for analysis and displays structured results in the UI. The response needs to be JSON so I can render it.</p>
<p>However longer inputs sometimes would generate errors. Longer inputs meant longer system prompts and longer responses.</p>
<p>The logs showed:</p>
<pre><code>Analysis failed: expected ',' or '}' after object value</code></pre>
<p>Claude was generating valid-looking JSON that wasn&#8217;t actually valid. A dropped comma deep in a large response object. The longer the response, the more likely this happened.</p>
<h2>The Old Approach: Prompt Engineering + Defensive Parsing</h2>
<p>My system prompt included a 28-line block demanding JSON output:</p>
<pre><code class="language-ruby">SYSTEM_PROMPT = &lt;&lt;~PROMPT
  ...
  CRITICAL INSTRUCTIONS:
  - You MUST ALWAYS respond with valid JSON. No exceptions. No explanations outside JSON.
  - NEVER respond with plain text - always use the JSON format.

  You MUST ALWAYS respond in JSON format with the following structure (no exceptions):
  {
    "score": 0-100,
    "level": "low|medium|high",
    "summary": "Brief overall assessment",
    "items": [
      {
        "title": "Issue name",
        "description": "What's wrong",
        "severity": "low|medium|high"
      }
    ],
    "recommendations": [
      "Specific actionable suggestion 1",
      "Specific actionable suggestion 2"
    ]
  }
PROMPT</code></pre>
<p>Despite all the shouting in the prompt, Claude would sometimes:</p>
<ul>
<li>Wrap the JSON in markdown code fences (<code>```json ... ```</code>)</li>
<li>Add explanatory text after the closing brace</li>
<li>Drop commas in deeply nested objects on long responses</li>
<li>Return plain text when it decided the input wasn&#8217;t suitable for analysis</li>
</ul>
<p>So I built a pipeline of defensive code to handle all of this.</p>
<p><strong>Step 1: Extract JSON from whatever Claude returned.</strong> A brace-matching parser that stripped markdown fences, found the first <code>{</code>, tracked nesting depth while respecting string escaping, and separated trailing notes:</p>
<pre><code class="language-ruby">def extract_json_and_note(text)
  text = text.strip
  if text.start_with?("```")
    text = text.sub(/A```(?:json|JSON)?s*/, "").sub(/s*```z/, "").strip
  end

  start_idx = text.index("{")
  return [text, nil, false] if start_idx.nil?

  brace_count = 0
  in_string = false
  escape_next = false

  text[start_idx..].each_char.with_index do |char, idx|
    if escape_next
      escape_next = false
      next
    end
    case char
    when "\" then escape_next = true if in_string
    when '"'  then in_string = !in_string unless escape_next
    when "{"  then brace_count += 1 unless in_string
    when "}"
      brace_count -= 1 unless in_string
      if brace_count == 0
        end_idx = start_idx + idx
        json_text = text[start_idx..end_idx]
        note = text[(end_idx + 1)..].strip.presence
        return [json_text, note, true]
      end
    end
  end

  [text, nil, false]
end</code></pre>
<p><strong>Step 2: Normalize missing fields</strong> because Claude might omit arrays for edge cases:</p>
<pre><code class="language-ruby">def normalize_response!(result)
  result["score"] ||= 0
  result["level"] ||= "unknown"
  result["summary"] ||= "Analysis complete"
  result["items"] ||= []
  result["recommendations"] ||= []
  result["score"] = result["score"].to_i if result["score"].is_a?(String)
end</code></pre>
<p><strong>Step 3: Validate the structure</strong> because even after parsing, I couldn&#8217;t trust it:</p>
<pre><code class="language-ruby">def validate_response_structure!(result)
  required_keys = %w[score level summary items recommendations]
  missing_keys = required_keys - result.keys
  raise "Invalid response structure: missing keys #{missing_keys.join(', ')}" if missing_keys.any?

  score = result["score"]
  unless score.is_a?(Integer) &amp;&amp; score &gt;= 0 &amp;&amp; score &lt;= 100
    raise "Invalid score: must be integer 0-100, got #{score.inspect}"
  end

  %w[items recommendations].each do |key|
    unless result[key].is_a?(Array)
      raise "Invalid #{key}: expected array, got #{result[key].class}"
    end
  end
end</code></pre>
<p>All of this existed because I was asking an LLM to format its own output as JSON via natural language instructions. I was writing a fragile parser for a format the model was never <em>constrained</em> to produce.</p>
<h2>The Fix: Tool Use</h2>
<p>Anthropic&#8217;s tool use API (also called function calling) lets you define a JSON schema that Claude <em>must</em> conform to. Instead of asking Claude to output JSON, you tell the API: &#8220;call this function with these typed parameters.&#8221; Claude&#8217;s response is guaranteed to match the schema.</p>
<p>Here&#8217;s the schema definition:</p>
<pre><code class="language-ruby">ANALYSIS_TOOL = {
  name: "analyze",
  description: "Return the structured analysis results",
  input_schema: {
    type: "object",
    required: ["score", "level", "summary", "items", "recommendations"],
    properties: {
      score: { type: "integer", description: "Overall score from 0 (safe) to 100 (dangerous)" },
      level: { type: "string", enum: ["low", "medium", "high"] },
      summary: { type: "string", description: "Brief overall assessment" },
      items: {
        type: "array",
        items: {
          type: "object",
          required: ["title", "description", "severity"],
          properties: {
            title:       { type: "string" },
            description: { type: "string" },
            severity:    { type: "string", enum: ["low", "medium", "high"] }
          }
        }
      },
      recommendations: { type: "array", items: { type: "string" } }
    }
  }
}.freeze</code></pre>
<p>The API call adds two parameters:</p>
<pre><code class="language-ruby">response = client.messages.create(
  model: model,
  max_tokens: max_tokens,
  system: [{ type: "text", text: system_prompt }],
  messages: [{ role: "user", content: user_message }],
  tools: [ANALYSIS_TOOL],
  tool_choice: { type: "tool", name: "analyze" }
)</code></pre>
<p><code>tools:</code> defines the schema. <code>tool_choice:</code> with <code>type: "tool"</code> forces Claude to use it — no chance of returning prose instead.</p>
<p>Response extraction is three lines:</p>
<pre><code class="language-ruby">tool_block = response.content.find { |b| b.type.to_s == "tool_use" }
raise "No tool_use block in response" unless tool_block
result = tool_block.input.transform_keys(&amp;:to_s)</code></pre>
<p>That&#8217;s it. <code>tool_block.input</code> is already a parsed hash. No <code>JSON.parse</code>, no brace matching, no markdown stripping, no comma repair.</p>
<h2>The Result</h2>
<p><strong>Deleted:</strong> ~160 lines from the service, ~250 lines from tests..</p>
<p><strong>Added:</strong> ~30 lines for the schema definition, 2 parameters on the API call, 3 lines of response extraction.</p>
<p>The system prompt shrank too. The 28 lines of &#8220;YOU MUST RESPOND IN JSON&#8221; instructions disappeared entirely. The prompt now focuses on <em>what</em> to analyze, not <em>how</em> to format the output.</p>
<p>The user message went from <code>"Analyze this and respond with JSON only:"</code> to just <code>"Analyze this:"</code>.</p>
<h2>When Should You Use This?</h2>
<p>Any time you want structured output from an LLM. If you&#8217;re writing regex to fix JSON commas, building brace-matching parsers, or adding &#8220;RESPOND IN JSON ONLY&#8221; to your prompts: switch to tool use. The schema is self-documenting, the output is guaranteed valid, and you delete code instead of writing it.</p>
<p>The one caveat: tool use constrains the <em>structure</em> but not the <em>content</em>. Claude can still put whatever it wants in a string field. You still need to validate that a score is in a sensible range or that enum values match your expectations. But &#8220;validate the values&#8221; is a much smaller problem than &#8220;parse arbitrary text that might be JSON.&#8221;</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.adamzolo.com/claude-api-tool-use-vs-prompting/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Idempotent Stripe Webhooks</title>
		<link>https://blog.adamzolo.com/idempotent-stripe-webhooks/</link>
					<comments>https://blog.adamzolo.com/idempotent-stripe-webhooks/#respond</comments>
		
		<dc:creator><![CDATA[Adam Zolo]]></dc:creator>
		<pubDate>Thu, 12 Feb 2026 11:37:48 +0000</pubDate>
				<category><![CDATA[Rails]]></category>
		<category><![CDATA[Stripe]]></category>
		<guid isPermaLink="false">https://blog.adamzolo.com/?p=1097</guid>

					<description><![CDATA[How to Implement Idempotent Stripe Webhooks in Rails Stripe can send the same webhook event more than once. Network timeouts, retries, and infrastructure hiccups all mean your endpoint might process the same event twice, charging a customer double, creating duplicate subscriptions, or corrupting your data. The fix is idempotency: making your webhook handler safe to&#8230;<p><a class="more-link" href="https://blog.adamzolo.com/idempotent-stripe-webhooks/" title="Continue reading &#8216;Idempotent Stripe Webhooks&#8217;">Continue reading <span class="meta-nav">&#8594;</span></a></p>]]></description>
										<content:encoded><![CDATA[
<h1 class="wp-block-heading">How to Implement Idempotent Stripe Webhooks in Rails</h1>



<p>Stripe can send the same webhook event more than once. Network timeouts, retries, and infrastructure hiccups all mean your endpoint might process the same event twice, charging a customer double, creating duplicate subscriptions, or corrupting your data.</p>



<p>The fix is idempotency: making your webhook handler safe to call multiple times with the same event.</p>



<h2 class="wp-block-heading">The Problem</h2>



<p>Every Stripe event has a unique ID like <code>evt_1abc123</code>. Stripe guarantees this ID is unique, but your endpoint has no such guarantee about delivery. From the <a href="https://docs.stripe.com/webhooks#handle-duplicate-events">Stripe docs</a>:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Webhook endpoints might occasionally receive the same event more than once. You can guard against duplicated event receipt by making your event processing idempotent.</p>
</blockquote>



<p>Without protection, a retried <code>invoice.paid</code> event could credit a user&#8217;s account twice or trigger duplicate emails.</p>



<h2 class="wp-block-heading">The Solution</h2>



<p>Track every processed event ID in your database. Before handling an event, check if you&#8217;ve already seen it. If yes, skip it.</p>



<h3 class="wp-block-heading">Step 1: Create the Table</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: ruby; title: ; notranslate">
# db/migrate/xxx_create_stripe_webhook_events.rb
class CreateStripeWebhookEvents &lt; ActiveRecord::Migration&#x5B;8.1]
  def change
    create_table :stripe_webhook_events do |t|
      t.string :stripe_event_id, null: false
      t.string :event_type
      t.datetime :processed_at
      t.timestamps
    end

    add_index :stripe_webhook_events, :stripe_event_id, unique: true
  end
end
</pre></div>


<p>The unique index on <code>stripe_event_id</code> is the key. Even if two requests arrive simultaneously, the database constraint guarantees only one INSERT succeeds.</p>



<h3 class="wp-block-heading">Step 2: Create the Model</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: ruby; title: ; notranslate">
# app/models/stripe_webhook_event.rb
class StripeWebhookEvent &lt; ApplicationRecord
  validates :stripe_event_id, presence: true, uniqueness: true

  def self.process(stripe_event_id, event_type:)
    create!(
      stripe_event_id: stripe_event_id,
      event_type: event_type,
      processed_at: Time.current
    )
    true
  rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
    false
  end
end
</pre></div>


<p><code>process</code> returns <code>true</code> if this is a new event, <code>false</code> if it&#8217;s a duplicate. We rescue both exceptions because:</p>



<ul class="wp-block-list">
<li><code>RecordNotUnique</code> — the DB unique constraint catches concurrent duplicate inserts</li>



<li><code>RecordInvalid</code> — the model-level uniqueness validation catches sequential duplicates</li>
</ul>



<h3 class="wp-block-heading">Step 3: Use It in Your Webhook Handler</h3>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
# app/services/stripe_webhook_handler.rb
def process!
  event = verify_signature!

  unless StripeWebhookEvent.process(event.id, event_type: event.type)
    Rails.logger.info(&quot;Skipping duplicate webhook: #{event.id}&quot;)
    return { status: &quot;success&quot;, duplicate: true }
  end

  handle_event(event)
end
</pre></div>


<p>The check goes right after signature verification and before any business logic. If it&#8217;s a duplicate, we return a success response (so Stripe doesn&#8217;t keep retrying) and skip processing.</p>



<h3 class="wp-block-heading">Step 4: Return 200 for Duplicates</h3>



<p>This is important — always return a 2xx status for duplicates:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: ruby; title: ; notranslate">
# app/controllers/webhooks_controller.rb
def stripe
  handler = StripeWebhookHandler.new(
    payload: request.body.read,
    signature: request.env&#x5B;&quot;HTTP_STRIPE_SIGNATURE&quot;]
  )

  result = handler.process!
  render json: result, status: :ok
rescue StripeWebhookHandler::WebhookError =&gt; e
  render json: { error: e.message }, status: :bad_request
end
</pre></div>


<p>If you return an error for a duplicate, Stripe will keep retrying — which is the opposite of what you want.</p>



<h2 class="wp-block-heading">Cleanup</h2>



<p>Over time, the <code>stripe_webhook_events</code> table will grow. Add a periodic cleanup job to prune old records:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: plain; title: ; notranslate">
# Keep 90 days of webhook history
StripeWebhookEvent.where(&quot;created_at &amp;lt; ?&quot;, 90.days.ago).delete_all
</pre></div>


<p>Stripe retries happen within hours, not months, so 90 days is more than enough.</p>



<h2 class="wp-block-heading">Summary</h2>



<ol class="wp-block-list">
<li>Create a table with a unique index on <code>stripe_event_id</code></li>



<li>Attempt an INSERT before processing — if it fails, it&#8217;s a duplicate</li>



<li>Always return 200 for duplicates so Stripe stops retrying</li>



<li>The database constraint handles race conditions that application-level checks can&#8217;t</li>
</ol>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.adamzolo.com/idempotent-stripe-webhooks/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Building Clausy: A Contract Analysis Tool with Rails 8 and Claude AI</title>
		<link>https://blog.adamzolo.com/building-clausy-a-contract-analysis-tool-with-rails-8-and-claude-ai/</link>
					<comments>https://blog.adamzolo.com/building-clausy-a-contract-analysis-tool-with-rails-8-and-claude-ai/#respond</comments>
		
		<dc:creator><![CDATA[Adam Zolo]]></dc:creator>
		<pubDate>Sun, 08 Feb 2026 21:07:19 +0000</pubDate>
				<category><![CDATA[AI]]></category>
		<category><![CDATA[Rails]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[Web]]></category>
		<guid isPermaLink="false">https://blog.adamzolo.com/?p=1090</guid>

					<description><![CDATA[I just launched https://clausyapp.com, a web app that uses AI to analyze contracts and highlight potential issues. You upload a PDF/images or paste text, and Claude AI reads through it to find things like unlimited liability clauses, auto-renewal terms, or aggressive IP assignment language. Why I Built This I&#8217;ve signed a enough contracts over the&#8230;<p><a class="more-link" href="https://blog.adamzolo.com/building-clausy-a-contract-analysis-tool-with-rails-8-and-claude-ai/" title="Continue reading &#8216;Building Clausy: A Contract Analysis Tool with Rails 8 and Claude AI&#8217;">Continue reading <span class="meta-nav">&#8594;</span></a></p>]]></description>
										<content:encoded><![CDATA[
<p>I just launched https://clausyapp.com, a web app that uses AI to analyze contracts and highlight potential issues. You upload a PDF/images or paste text, and Claude AI reads through it to find things like unlimited liability clauses, auto-renewal terms, or aggressive IP assignment language.</p>



<h2 class="wp-block-heading">Why I Built This</h2>



<p>I&#8217;ve signed a enough contracts over the years, and I was never quite sure if I was missing something important buried in the legal language. I&#8217;d skim through them, but let&#8217;s be honest – I didn&#8217;t understand half of it. Getting a lawyer to review every contract is also just not something I&#8217;m going to do, unless it&#8217;s something really big.</p>



<p>I figured: AI is pretty good at reading and understanding text now. And based on how many of these AI contract analysis tools are out there, it&#8217;s the new TODO app in the age of AI.</p>



<h2 class="wp-block-heading">The Stack</h2>



<p>I went with a Rails 8 monolith because I wanted something I could ship quickly and maintain solo:</p>



<ul class="wp-block-list">
<li>Rails 8 with Hotwire (Turbo + Stimulus)</li>



<li>Anthropic&#8217;s Claude API</li>



<li>Solid Queue for background jobs with priority queues (paid users get faster processing)</li>



<li>Solid Cache for caching and rate limits</li>



<li>Stripe for subscriptions and billing</li>



<li>Tesseract OCR for extracting text from scanned images (JPG, PNG, WebP, HEIC)</li>



<li>Kamal for deployment</li>
</ul>



<p>Everything runs in Docker containers. No separate frontend framework, no microservices. Just a straightforward Rails app that does one thing well.</p>



<h2 class="wp-block-heading">Technical Security Challenges</h2>



<ol class="wp-block-list">
<li><strong>File Processing Security</strong>
<ul class="wp-block-list">
<li>Magic byte validation – Don&#8217;t trust file extensions. I check the actual file signature to verify it&#8217;s really a PDF or DOCX.</li>



<li>Size limits – DOCX files are zip archives, so I enforce size limits before decompression to prevent zip bombs.</li>



<li>Immediate deletion – Original files are deleted right after text extraction. No long-term storage of sensitive documents.</li>



<li>Command injection prevention – Only use safe extraction tools, never shell out with user-provided filenames.</li>
</ul>
</li>



<li><strong>Server Access</strong>
<ul class="wp-block-list">
<li>Firewall at the provider level </li>



<li>Firewall at the node level (ufw)</li>



<li>ssh through certs only, limit access to specific IP&#8217;s</li>



<li>Cloudflare</li>
</ul>
</li>



<li><strong>Application </strong>Security
<ul class="wp-block-list">
<li>Devise authentication &#8211; Industry-standard auth framework</li>



<li>CSRF protection &#8211; Rails CSRF tokens on all POST/PUT/PATCH/DELETE requests</li>



<li>UUID-based URLs &#8211; Guest contracts use UUIDs (prevents enumeration attacks)</li>



<li>Rate Limits (Rack::Attack)</li>



<li>CSP Policy
<ul class="wp-block-list">
<li>No unsafe-eval &#8211; Prevents eval() attacks</li>



<li>Whitelisted script sources &#8211; Only self, HTTPS, Stripe, Cloudflare allowed</li>



<li>No object embeds &#8211; object_src :none blocks Flash/plugin attacks</li>



<li>Nonce-based scripts &#8211; Importmap scripts use session-based nonces</li>



<li>HTTPS enforced &#8211; All resources loaded over HTTPS</li>
</ul>
</li>



<li>Input Validation</li>
</ul>
</li>



<li>Fraud Prevention
<ul class="wp-block-list">
<li>Email history tracking &#8211; SHA256 email hashing &#8211; Email hashes stored, not plain emails</li>
</ul>
</li>



<li>Payment Security
<ul class="wp-block-list">
<li>Stripe webhook verification &#8211; Signature validation on all webhook events</li>



<li>No card storage &#8211; Stripe handles all payment details</li>
</ul>
</li>



<li>Secret Management
<ul class="wp-block-list">
<li>Rails credentials &#8211; All secrets in encrypted credentials.yml.enc</li>
</ul>
</li>



<li>XSS Prevention
<ul class="wp-block-list">
<li>Automatic HTML escaping</li>



<li>CSP headers &#8211; Content Security Policy blocks inline scripts</li>
</ul>
</li>



<li>Transport Security
<ul class="wp-block-list">
<li>HTTPS everywhere &#8211; All resources loaded over HTTPS</li>



<li>Secure cookies &#8211; Session cookies marked secure in production</li>



<li>HSTS headers &#8211; Forces HTTPS connections</li>
</ul>
</li>



<li>DoS Prevention
<ul class="wp-block-list">
<li>Job queues &#8211; Background processing prevents request timeouts</li>



<li>Priority queues &#8211; Paid users get separate high-priority queue</li>



<li>Rate limiting &#8211; Comprehensive rate limits across all endpoints</li>



<li>Query optimization &#8211; Indexed queries prevent slow lookups</li>
</ul>
</li>
</ol>



<h2 class="wp-block-heading">Try it</h2>



<p>Demo (no signup): <a href="https://clausyapp.com/contracts/new?demo=hn">https://clausyapp.com/contracts/new?demo=hn</a></p>



<p>Full app: <a href="https://clausyapp.com">https://clausyapp.com</a></p>



<p>It&#8217;s not legal advice – I&#8217;m very explicit about that – but it can help you spot things you might want to ask a lawyer about.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.adamzolo.com/building-clausy-a-contract-analysis-tool-with-rails-8-and-claude-ai/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>How We Fixed the &#8220;First Web Container is Unhealthy&#8221; Error: A DNS Deep Dive</title>
		<link>https://blog.adamzolo.com/how-we-fixed-the-first-web-container-is-unhealthy-error-a-dns-deep-dive/</link>
					<comments>https://blog.adamzolo.com/how-we-fixed-the-first-web-container-is-unhealthy-error-a-dns-deep-dive/#respond</comments>
		
		<dc:creator><![CDATA[Adam Zolo]]></dc:creator>
		<pubDate>Mon, 26 Jan 2026 16:10:56 +0000</pubDate>
				<category><![CDATA[docker]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Rails]]></category>
		<category><![CDATA[Uncategorized]]></category>
		<guid isPermaLink="false">https://blog.adamzolo.com/?p=1082</guid>

					<description><![CDATA[The Error That Nearly Broke Our Deployment Three hours into our Kamal deployment, we were stuck in a loop: ERROR Failed to boot web on {ip_address} INFO First web container is unhealthy on {ip_address}, not booting any other roles The container would start, but Kamal&#8217;s health check kept failing. After 30 seconds, Kamal would kill&#8230;<p><a class="more-link" href="https://blog.adamzolo.com/how-we-fixed-the-first-web-container-is-unhealthy-error-a-dns-deep-dive/" title="Continue reading &#8216;How We Fixed the &#8220;First Web Container is Unhealthy&#8221; Error: A DNS Deep Dive&#8217;">Continue reading <span class="meta-nav">&#8594;</span></a></p>]]></description>
										<content:encoded><![CDATA[<br />
<h2>The Error That Nearly Broke Our Deployment</h2>
<p>Three hours into our Kamal deployment, we were stuck in a loop:</p>
<pre><code>ERROR Failed to boot web on {ip_address}
  INFO First web container is unhealthy on {ip_address}, not booting any other roles</code></pre>
<p>The container would start, but Kamal&#8217;s health check kept failing. After 30 seconds, Kamal would kill the container<br />
   and retry, creating an endless loop.</p>
<p>We spent hours debugging deployment scripts, PostgreSQL configurations, and Rails settings. The fix turned out to<br />
  be much simpler: DNS configuration.</p>
<h2>The Root Cause: Broken DNS Resolution</h2>
<h3>What Was Happening</h3>
<p>When Kamal tried to verify container health, it performed this sequence:</p>
<ol>
<li>Container starts → my_app-web-abc123 boots</li>
<li>Traefik (Kamal proxy) tries to check /up endpoint</li>
<li>DNS lookup → Resolve my_app-web-abc123 to an IP address</li>
<li>Health check fails → DNS resolution times out or fails</li>
<li>Container killed → Kamal marks it as unhealthy</li>
</ol>
<h3>The DNS Failure</h3>
<p>The Traefik container&#8217;s /etc/resolv.conf showed:</p>
<pre><code>nameserver 127.0.0.53
  search members.linode.com
  options edns0 trust-ad ndots:0</code></pre>
<p><strong>Problem:</strong> 127.0.0.53 is the host&#8217;s systemd-resolved DNS server. It&#8217;s not accessible from inside<br />
  Docker containers!</p>
<p>When Traefik tried to resolve my_app-web-abc123:</p>
<ul>
<li>It queried 127.0.0.53 (systemd-resolved)</li>
<li>The query failed with &#8220;connection refused&#8221;</li>
<li>Health check failed</li>
<li>Container was killed</li>
</ul>
<h2>The Solution: Proper Docker DNS Configuration</h2>
<h3>What We Fixed</h3>
<p>We configured Docker&#8217;s DNS settings in <code>/etc/docker/daemon.json</code>:</p>
<pre><code>{
    "dns": ["127.0.0.11", "8.8.8.8", "1.1.1.1"]
  }</code></pre>
<h3>Why This Works</h3>
<p><strong>1. 127.0.0.11 (Docker&#8217;s Internal DNS) &#8211; First Priority</strong></p>
<ul>
<li>Resolves container hostnames automatically</li>
<li>Handles inter-container communication</li>
<li>Always available inside Docker networks</li>
</ul>
<p><strong>2. 8.8.8.8 (Google DNS) &#8211; Second Priority</strong></p>
<ul>
<li>Resolves external domains (APIs, gems, etc.)</li>
<li>Fast and reliable</li>
<li>Global infrastructure</li>
</ul>
<p><strong>3. 1.1.1.1 (Cloudflare DNS) &#8211; Third Priority</strong></p>
<ul>
<li>Privacy-focused external DNS</li>
<li>Backup if 8.8.8.8 fails</li>
<li>No query logging</li>
</ul>
<h3>How Docker Uses This</h3>
<p>Docker&#8217;s DNS resolution order:</p>
<ol>
<li>Try 127.0.0.11 (internal) → container names</li>
<li>If that fails → 8.8.8.8 (external) → domains</li>
<li>If that fails → 1.1.1.1 (external) → domains</li>
</ol>
<h2>The IPv4/IPv6 Issue</h2>
<p>While debugging, we discovered another subtle problem:</p>
<h3>The IPv6 Trap</h3>
<p>The server setup script used:</p>
<pre><code>SERVER_IP=$(curl -s ifconfig.me || echo "ip_address_goes_here")</code></pre>
<p><strong>Problem:</strong> ifconfig.me returned an IPv6 address:</p>
<pre><code>2600:3c03::...</code></pre>
<p>This IPv6 address was used in PostgreSQL&#8217;s pg_hba.conf:</p>
<pre><code>host my_app_production my_app_user 2600:3c03.../32 md5</code></pre>
<p>PostgreSQL had issues with this IPv6 address, causing authentication failures.</p>
<h3>The Fix</h3>
<p>Force IPv4 detection:</p>
<pre><code>SERVER_IP=$(curl -s -4 ifconfig.me || echo "ip_address_goes_here")</code></pre>
<p>The <code>-4</code> flag ensures we always get an IPv4 address, which PostgreSQL handles reliably.</p>
<h2>The PostgreSQL Network Isolation Issue</h2>
<h3>The Problem</h3>
<p>Kamal uses a separate Docker network (172.18.0.0/16) for containers, while PostgreSQL is on the host&#8217;s Docker<br />
  bridge network (172.17.0.0/16).</p>
<p>The firewall only allowed 172.17.0.0/16:</p>
<pre><code>5432/tcp  ALLOW  172.17.0.0/16</code></pre>
<h3>The Fix</h3>
<p>Add the Kamal network to both firewall and PostgreSQL config:</p>
<p><strong>Firewall (ufw):</strong></p>
<pre><code>sudo ufw allow from 172.18.0.0/16 to any port 5432</code></pre>
<p><strong>PostgreSQL (pg_hba.conf):</strong></p>
<pre><code>host my_app_production my_app_user 172.18.0.0/16 md5</code></pre>
<h2>Complete Fix in our setup script</h2>
<h3>IPv4 Fix </h3>
<pre><code>SERVER_IP=$(curl -s -4 ifconfig.me || echo "ip_address_goes_here")</code></pre>
<h3>Kamal Network Firewall Rule</h3>
<pre><code>sudo ufw allow from 172.18.0.0/16 to any port 5432</code></pre>
<h3>PostgreSQL Kamal Network Rule </h3>
<pre><code>host $DB_NAME $DB_USER 172.18.0.0/16 md5</code></pre>
<h3>Docker DNS Configuration </h3>
<pre><code>{
    "dns": ["127.0.0.11", "8.8.8.8", "1.1.1.1"]
  }</code></pre>
<h2>Key Takeaways</h2>
<ol>
<li><strong>DNS is Critical for Container Orchestration</strong>
<ul>
<li>Always configure Docker&#8217;s DNS properly</li>
<li>Include both internal and external DNS servers</li>
<li>Test DNS resolution from containers</li>
</ul>
</li>
<li><strong>Network Isolation Matters</strong>
<ul>
<li>Docker networks are isolated by default</li>
<li>PostgreSQL must allow connections from all Docker networks</li>
<li>Firewall rules must match</li>
</ul>
</li>
<li><strong>IPv4 vs IPv6 Can Break Things</strong>
<ul>
<li>PostgreSQL works better with IPv4</li>
<li>Force IPv4 when detecting server IPs</li>
<li>Test both IPv4 and IPv6 connectivity</li>
</ul>
</li>
<li><strong>Health Checks are Essential</strong>
<ul>
<li>The /up endpoint is critical for Kamal</li>
<li>DNS must work for health checks to succeed</li>
<li>Timeout settings matter (30s default)</li>
</ul>
</li>
</ol>
<h2>Troubleshooting DNS Issues</h2>
<p>If you encounter &#8220;First web container is unhealthy&#8221;:</p>
<ol>
<li><strong>Check Container Logs</strong><br />
  <code>docker logs my_app-web-abc123</code></li>
<li><strong>Check Traefik/Kamal Proxy Logs</strong><br />
  <code>docker logs kamal-proxy | grep -i healthcheck</code></li>
<li><strong>Test DNS Resolution</strong>
<pre><code># From inside Traefik container
  docker exec kamal-proxy getent hosts my_app-web-abc123
  docker exec kamal-proxy getent hosts google.com</code></pre>
</li>
<li><strong>Verify DNS Configuration</strong>
<pre><code># Check daemon.json
  cat /etc/docker/daemon.json

  # Check container's resolv.conf
  docker exec kamal-proxy cat /etc/resolv.conf</code></pre>
</li>
<li><strong>Check PostgreSQL Connectivity</strong>
<pre><code># From kamal network
  docker run --rm --network kamal postgres:16 psql \
    -h 172.17.0.1 -U my_app_user -d my_app_production -c "SELECT 1"</code></pre>
</li>
</ol>
<h2>Results</h2>
<p>After implementing all fixes:</p>
<ul>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> DNS resolution works (internal and external)</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Health checks pass (Traefik can reach containers)</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> PostgreSQL connections work (from both Docker networks)</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> Deployments succeed (consistent, reliable)</li>
<li><img src="https://s.w.org/images/core/emoji/17.0.2/72x72/2705.png" alt="✅" class="wp-smiley" style="height: 1em; max-height: 1em;" /> IPv4 detection works (no IPv6 issues)</li>
</ul>
<h2>Final Thoughts</h2>
<p>The &#8220;First web container is unhealthy&#8221; error can be a DNS configuration issue, not a deployment or application<br />
  problem.</p>
<p>By understanding how Docker networks work, how DNS resolution functions, and how PostgreSQL authentication works, we can prevent this issue from ever occurring again.</p>
<p><strong>Key files to review:</strong></p>
<ul>
<li><code>/etc/docker/daemon.json</code> &#8211; Docker DNS configuration</li>
<li><code>/etc/postgresql/16/main/pg_hba.conf</code> &#8211; PostgreSQL authentication</li>
<li><code>/etc/ufw/rules.conf</code> &#8211; Firewall rules</li>
</ul>
<p>The fix is now automated in our setup script, ensuring new servers have proper DNS and network configuration from<br />
  day one.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.adamzolo.com/how-we-fixed-the-first-web-container-is-unhealthy-error-a-dns-deep-dive/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Homelab Setup</title>
		<link>https://blog.adamzolo.com/homelab-setup/</link>
					<comments>https://blog.adamzolo.com/homelab-setup/#respond</comments>
		
		<dc:creator><![CDATA[Adam Zolo]]></dc:creator>
		<pubDate>Sun, 25 Aug 2024 16:22:29 +0000</pubDate>
				<category><![CDATA[Homelab]]></category>
		<guid isPermaLink="false">https://blog.adamzolo.com/?p=1064</guid>

					<description><![CDATA[]]></description>
										<content:encoded><![CDATA[
<ul class="wp-block-list">
<li>Router
<ul class="wp-block-list">
<li>PfSense running on Protectli fw4b
<ul class="wp-block-list">
<li>pfBlockerNG for whole house ads blocking</li>



<li>3 subnets: one for guest wifi, one for IoT, one for the rest of the devices</li>



<li>haproxy &#8211; a reverse proxy to help with remote access to my house cameras</li>



<li>Dynamic DNS setup with Dynu DNS</li>
</ul>
</li>
</ul>
</li>



<li>Switch
<ul class="wp-block-list">
<li>Unifi with PoE for the cameras</li>
</ul>
</li>



<li>WiFi
<ul class="wp-block-list">
<li>Unifi Access Points</li>
</ul>
</li>



<li>NAS
<ul class="wp-block-list">
<li>TrueNAS</li>
</ul>
</li>



<li>Cameras
<ul class="wp-block-list">
<li>Blue Iris running as a Windows service</li>
</ul>
</li>



<li>Home Automation
<ul class="wp-block-list">
<li>SmartThings -> migrated to Home Assistant</li>
</ul>
</li>
</ul>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.adamzolo.com/homelab-setup/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Rails Migration to Change string to boolean (PostgreSQL)</title>
		<link>https://blog.adamzolo.com/rails-migration-to-change-string-to-boolean-postgresql/</link>
					<comments>https://blog.adamzolo.com/rails-migration-to-change-string-to-boolean-postgresql/#respond</comments>
		
		<dc:creator><![CDATA[Adam Zolo]]></dc:creator>
		<pubDate>Fri, 19 Jan 2024 20:47:21 +0000</pubDate>
				<category><![CDATA[Rails]]></category>
		<category><![CDATA[SQL]]></category>
		<guid isPermaLink="false">https://blog.adamzolo.com/?p=1057</guid>

					<description><![CDATA[When you run the migration to change the the column type from string to boolean, you may encounter this kind of error: This just tells you that you need a rule to convert your string to boolean. You can fix with using synthax. For example, if you want all columns to change to false: Or&#8230;<p><a class="more-link" href="https://blog.adamzolo.com/rails-migration-to-change-string-to-boolean-postgresql/" title="Continue reading &#8216;Rails Migration to Change string to boolean (PostgreSQL)&#8217;">Continue reading <span class="meta-nav">&#8594;</span></a></p>]]></description>
										<content:encoded><![CDATA[
<p>When you run the migration to change the the column type from string to boolean, you may encounter this kind of error:</p>



<pre class="wp-block-code"><code>PG::DatatypeMismatch: ERROR:  column "blah" cannot be cast automatically to type boolean
HINT:  You might need to specify "USING blah::boolean".</code></pre>



<p>This just tells you that you need a rule to convert your string to boolean. You can fix with <code>using</code> synthax. For example, if you want all columns to change to false:</p>



<pre class="wp-block-code"><code>change_table :table_name do |t|
  t.change :column_name, :boolean, using: 'false', default: false, null: false
end</code></pre>



<p>Or if you want to convert your existing values from your column, you could do something like this:</p>



<pre class="wp-block-code"><code>change_table :table_name do |t|
  t.change :column_name, :boolean, using: 'cast(column_name as boolean)', default: false, null: false
end</code></pre>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.adamzolo.com/rails-migration-to-change-string-to-boolean-postgresql/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Comparing PostgreSQL timestamps without timezones with dates with a timezone</title>
		<link>https://blog.adamzolo.com/comparing-postgresql-timestamps-without-timezones-with-dates-with-a-timezone/</link>
					<comments>https://blog.adamzolo.com/comparing-postgresql-timestamps-without-timezones-with-dates-with-a-timezone/#respond</comments>
		
		<dc:creator><![CDATA[Adam Zolo]]></dc:creator>
		<pubDate>Thu, 12 Jan 2023 23:04:33 +0000</pubDate>
				<category><![CDATA[SQL]]></category>
		<guid isPermaLink="false">https://blog.adamzolo.com/?p=1041</guid>

					<description><![CDATA[Problem: you have a timestamp field and you need to compare it with something that has a timezone. Let&#8217;s assume your database default timezone is UTC (can check it with show timezone;). Thus, we are making the assumption that your dates are stored in UTC. Since timestamp does not have any timezone data, we first&#8230;<p><a class="more-link" href="https://blog.adamzolo.com/comparing-postgresql-timestamps-without-timezones-with-dates-with-a-timezone/" title="Continue reading &#8216;Comparing PostgreSQL timestamps without timezones with dates with a timezone&#8217;">Continue reading <span class="meta-nav">&#8594;</span></a></p>]]></description>
										<content:encoded><![CDATA[
<p>Problem: you have a <code>timestamp</code> field and you need to compare it with something that has a timezone.</p>



<p>Let&#8217;s assume <strong>your database default timezone is UTC</strong> (can check it with <code>show timezone</code>;). Thus, we are making the assumption that your dates are stored in UTC.</p>



<p>Since <code>timestamp</code> does not have any timezone data, we first need to read it in UTC, so it adds the timezone data. Then we can convert it to the desired timezone:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
select timestamp_field AT TIME ZONE &#039;UTC&#039; AT TIME ZONE &#039;America/New_York&#039;
</pre></div>


<p>Now, we have the timezone and proper daylight-saving offset.</p>



<p>Just for fun, let&#8217;s check if the current hour matches the hour from the saved timestamp field in the &#8216;America/New_York&#8217; timezone:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
EXTRACT ( HOUR FROM timestamp_field at time zone &#039;UTC&#039; at time zone &#039;America/New_York&#039;) = EXTRACT ( HOUR FROM now() at time zone &#039;America/New_York&#039;)
</pre></div>


<p>Notice, that we do not read now() in UTC timezone first, because our DB default timezone is UTC and now() already has all of the timezone data.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.adamzolo.com/comparing-postgresql-timestamps-without-timezones-with-dates-with-a-timezone/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Setting up a free Minecraft Server on Oracle Cloud</title>
		<link>https://blog.adamzolo.com/setting-up-a-free-minecraft-server-on-oracle-cloud/</link>
					<comments>https://blog.adamzolo.com/setting-up-a-free-minecraft-server-on-oracle-cloud/#respond</comments>
		
		<dc:creator><![CDATA[Adam Zolo]]></dc:creator>
		<pubDate>Sun, 26 Jun 2022 15:52:09 +0000</pubDate>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Minecraft]]></category>
		<guid isPermaLink="false">https://blog.adamzolo.com/?p=1019</guid>

					<description><![CDATA[Update 2 months later Somewhat unsurprisingly, given Oracle reputation, they disabled my supposedly always free resources two months later. Turns out, they disable the resources created while you were on trial. However, you can still make a clone of your boot volume, then terminate your previous instance, then recreate your instance from the cloned volume.&#8230;<p><a class="more-link" href="https://blog.adamzolo.com/setting-up-a-free-minecraft-server-on-oracle-cloud/" title="Continue reading &#8216;Setting up a free Minecraft Server on Oracle Cloud&#8217;">Continue reading <span class="meta-nav">&#8594;</span></a></p>]]></description>
										<content:encoded><![CDATA[
<h2 class="wp-block-heading">Update 2 months later</h2>



<p>Somewhat unsurprisingly, given Oracle reputation, they disabled my supposedly always free resources two months later. Turns out, they disable the resources created while you were on trial. However, you can still make a clone of your boot volume, then terminate your previous instance, then recreate your instance from the cloned volume. It works so far.</p>



<p></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>In this tutorial, we&#8217;ll set up a free Minecraft server using a free tier Oracle Cloud.</p>



<h2 class="wp-block-heading">Setting up an Oracle Free Tier</h2>



<p><a rel="noreferrer noopener" href="https://www.oracle.com/cloud/sign-in.html" target="_blank">Signup for Oracle free account</a>. Since this is Oracle, you probably want to check on the billing from time to time to make sure they didn&#8217;t start your changing for what they promised to be free.</p>



<p>Create a new compute instance. For max compatibility, we&#8217;ll use Ubuntu 18.04. You can try your luck with a newer version.</p>



<p> For the shape, select Ampere with 4 OCPU and 6GB Ram. This should still qualify for the free tier and should still be plenty for a Minecraft server.</p>



<p>Add your public SSH keys in the &#8220;Add SSH keys&#8221; section.</p>



<p>Create the instance. </p>



<p>After it&#8217;s created, go to the instance details, find &#8220;Primary VNIC&#8221; section, and open the subnet link (or create a new one).</p>



<p>Open Default Security List (or create a new one if one doesn&#8217;t exist yet)</p>



<p>Add Ingress Rules to open TCP/UDP ports 19132 for Bedrock and 25565 for Java edition (or both). Use CIDR for Source Type, 0.0.0.0/0 for Source CIDR, 19132 for Destination port. Repeat for TCP. Repeat for 25565 if planning to use Java edition.</p>



<p></p>



<p>SSH to your server</p>



<p>Upgrade all packages:</p>



<pre class="wp-block-code has-white-background-color has-background"><code>sudo apt-get update
sudo apt-get upgrade</code></pre>



<p>Let&#8217;s reset the firewall rules and open the ssh and Minecraft ports:</p>



<pre class="wp-block-code"><code>sudo iptables -P INPUT ACCEPT
sudo iptables -P FORWARD ACCEPT
sudo iptables -P OUTPUT ACCEPT
sudo iptables -F
sudo iptables-save
sudo ufw default deny incoming
sudo ufw default allow outgoing
sudo ufw allow 22/tcp
sudo ufw allow 19132/udp
sudo ufw allow 19132/tcp
sudo ufw allow 25565/udp
sudo ufw allow 25565/tcp
sudo ufw enable
sudo ufw status</code></pre>



<p>Optionally install zsh and vim:</p>



<pre class="wp-block-code"><code>sudo apt install zsh<br>sudo apt install vim</code></pre>



<h2 class="wp-block-heading">Bedrock</h2>



<p>SSH to your server: <code>ssh ubuntu@ip_address</code></p>



<p>We&#8217;ll use this script to setup the server: <a href="https://github.com/TheRemote/MinecraftBedrockServer">https://github.com/TheRemote/MinecraftBedrockServer</a></p>



<pre class="wp-block-code"><code>curl https://raw.githubusercontent.com/TheRemote/MinecraftBedrockServer/master/SetupMinecraft.sh | bash</code></pre>



<p>Use the following commands to start/restart the service (you specified the service name when you ran the installation script):</p>



<p></p>



<p>sudo systemctl stop minecraft_service<br>sudo systemctl start minecraft_service<br>sudo systemctl restart minecraft_service</p>



<pre class="wp-block-code"><code>screen -R
# To disconnect (do not do ctrl-c or it may kill the minecraft service):
ctrl-A-D</code></pre>



<h2 class="wp-block-heading">Java Edition</h2>



<p>This section is only for Java edition. Don&#8217;t use it if you need the Bedrock version.</p>



<p>We&#8217;ll use this script to install the Java edition Paper server: <a rel="noreferrer noopener" href="https://github.com/TheRemote/RaspberryPiMinecraft" target="_blank">https://github.com/TheRemote/RaspberryPiMinecraft</a>.</p>



<p>SSH to your server and run <code>curl https://raw.githubusercontent.com/TheRemote/RaspberryPiMinecraft/master/SetupMinecraft.sh | bash</code>. All default settings should be fine.</p>



<p></p>



<p>If you want Bedrock users to use your server, we can install Geyser plugin:</p>



<p>We&#8217;ll download the latest Geyser-Spigot server from <a href="https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/">https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/</a>. </p>



<p>On the server run:</p>



<pre class="wp-block-code"><code>cd minecraft/plugins/
curl -O https://ci.opencollab.dev/job/GeyserMC/job/Geyser/job/master/lastSuccessfulBuild/artifact/bootstrap/spigot/target/Geyser-Spigot.jar</code></pre>



<p>If you also want Bedrock users to be able to login with their Microsoft account without requiring a separate Java account, we can also install a floodgate plugin:</p>



<pre class="wp-block-code"><code>curl -O https://ci.opencollab.dev/job/GeyserMC/job/Floodgate/job/master/lastSuccessfulBuild/artifact/spigot/build/libs/floodgate-spigot.jar

Restart the Minecraft server with <code>sudo systemctl restart minecraft.service</code></code></pre>



<h2 class="wp-block-heading">DNS Records with Cloudflare</h2>



<p>Add an A record for your server IP. For example, if you own example.com and want to connect to your server using mct.example.com, then add an A record for mct pointing to your server IP.</p>



<p>Add 2 srv records for each port (19132 and 25565):</p>



<p>Use <code>mct</code> for Name, <code>_minecraft</code> for service, 0 for both Priority and Weight, UDP for protocol, <code>mct.example.com</code> for the target. <code>mct</code> is just the subdomain that you can change to whatever you want. Repeat for TCP and then for each port (19132 and 25565)</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.adamzolo.com/setting-up-a-free-minecraft-server-on-oracle-cloud/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>The Cost of PostgreSQL Foreign Keys</title>
		<link>https://blog.adamzolo.com/the-cost-of-postgresql-foreign-keys/</link>
					<comments>https://blog.adamzolo.com/the-cost-of-postgresql-foreign-keys/#respond</comments>
		
		<dc:creator><![CDATA[Adam Zolo]]></dc:creator>
		<pubDate>Thu, 10 Feb 2022 22:38:00 +0000</pubDate>
				<category><![CDATA[Performance]]></category>
		<category><![CDATA[SQL]]></category>
		<guid isPermaLink="false">https://blog.adamzolo.com/?p=998</guid>

					<description><![CDATA[Acknowledgments: shout out to Steven Jones at Syncro for helping me better understand how Foreign Keys work in PostgreSQL. Foreign keys are great for maintaining data integrity. However, they are not free. As mentioned in this blog post by Shaun Thomas: In PostgreSQL, every foreign key is maintained with an invisible system-level trigger added to&#8230;<p><a class="more-link" href="https://blog.adamzolo.com/the-cost-of-postgresql-foreign-keys/" title="Continue reading &#8216;The Cost of PostgreSQL Foreign Keys&#8217;">Continue reading <span class="meta-nav">&#8594;</span></a></p>]]></description>
										<content:encoded><![CDATA[
<p></p>



<p><strong>Acknowledgments</strong>: shout out to <a rel="noreferrer noopener" href="https://www.linkedin.com/in/steven-jones-a732567/" target="_blank">Steven Jones</a> at <a href="https://syncromsp.com/" target="_blank" rel="noreferrer noopener">Syncro</a> for helping me better understand how Foreign Keys work in PostgreSQL.</p>



<p></p>



<p>Foreign keys are great for maintaining data integrity. However, they are not free.</p>



<p>As mentioned in this blog post by Shaun Thomas:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow"><p>In PostgreSQL, every foreign key is maintained with an invisible system-level trigger added to the&nbsp;<em>source</em>&nbsp;table in the reference. At least one trigger must go here, as operations that modify the source data must be checked that they do not violate the constraint.</p><cite>https://bonesmoses.org/2014/05/14/foreign-keys-are-not-free/</cite></blockquote>



<p>What this means is that when you modify your parent table, even if you don&#8217;t touch any of the referred keys, these triggers are still fired. </p>



<p>In the original post from 2014, the overhead made the updates up to 95% slower with 20 foreign keys. Let&#8217;s see if things changed since then.</p>



<p></p>



<p>These are slightly updated scripts we&#8217;ll use for testing</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
CREATE OR REPLACE FUNCTION fnc_create_check_fk_overhead(key_count INT)
RETURNS VOID AS
$$
DECLARE
  i INT;
BEGIN
  CREATE TABLE test_fk
  (
    id   BIGINT PRIMARY KEY,
    junk VARCHAR
  );

  INSERT INTO test_fk
  SELECT generate_series(1, 100000), repeat(&#039; &#039;, 20);

  CLUSTER test_fk_pkey ON test_fk;

  FOR i IN 1..key_count LOOP
    EXECUTE &#039;CREATE TABLE test_fk_ref_&#039; || i || 
            &#039; (test_fk_id BIGINT REFERENCES test_fk (id))&#039;;
						
  END LOOP;

END;
$$ LANGUAGE plpgsql VOLATILE;


CREATE OR REPLACE FUNCTION fnc_check_fk_overhead(key_count INT)
RETURNS VOID AS
$$
DECLARE
  i INT;
BEGIN
  FOR i IN 1..100000 LOOP
    UPDATE test_fk SET junk = &#039;    blah                &#039;
     WHERE id = i;
  END LOOP;

END;
$$ LANGUAGE plpgsql VOLATILE;


CREATE OR REPLACE FUNCTION clean_up_overhead(key_count INT)
RETURNS VOID AS
$$
DECLARE
  i INT;
BEGIN
  DROP TABLE test_fk CASCADE;

  FOR i IN 1..key_count LOOP
    EXECUTE &#039;DROP TABLE test_fk_ref_&#039; || i;
  END LOOP;
END;
$$ LANGUAGE plpgsql VOLATILE;
</pre></div>


<p>To validate that the overhead is caused strictly by the presence of the foreign keys, and not from the cost of looking up the child records, after the first benchmark, we&#8217;ll modify the first function and add indexes on each foreign key:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
CREATE OR REPLACE FUNCTION fnc_create_check_fk_overhead(key_count INT)
RETURNS VOID AS
$$
DECLARE
  i INT;
BEGIN
  CREATE TABLE test_fk
  (
    id   BIGINT PRIMARY KEY,
    junk VARCHAR
  );

  INSERT INTO test_fk
  SELECT generate_series(1, 100000), repeat(&#039; &#039;, 20);

  CLUSTER test_fk_pkey ON test_fk;

  FOR i IN 1..key_count LOOP
    EXECUTE &#039;CREATE TABLE test_fk_ref_&#039; || i || 
            &#039; (test_fk_id BIGINT REFERENCES test_fk (id))&#039;;
						
		EXECUTE &#039;CREATE index test_fk_ref_index_&#039; || i ||
            &#039; on test_fk_ref_&#039; || i || &#039;(test_fk_id)&#039;;
  END LOOP;

END;
$$ LANGUAGE plpgsql VOLATILE;
</pre></div>


<p></p>



<p>We&#8217;ll run on on Mac i9, 2.3 GHz 8-Core, 64 GB Ram</p>



<p>PostgreSQL version 12</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: sql; title: ; notranslate">
-- without an index

select fnc_create_check_fk_overhead(0);
SELECT fnc_check_fk_overhead(0); -- 2.6-2.829
select clean_up_overhead(0)


select fnc_create_check_fk_overhead(20);
SELECT fnc_check_fk_overhead(20); -- 3.186-3.5. ~20% drop
select clean_up_overhead(20)


-- after updating our initial function to add an index for each foreign key:
select fnc_create_check_fk_overhead(0);
SELECT fnc_check_fk_overhead(0); -- 2.6-2.8
select clean_up_overhead(0)

select fnc_create_check_fk_overhead(20);
SELECT fnc_check_fk_overhead(20); -- 3.1 same ~20% drop
select clean_up_overhead(20)


</pre></div>


<p>As we see from the benchmark, the drop in update performance on a parent table is about 20% after adding 20 tables with a foreign key to the parent. It&#8217;s not quite as bad as <meta charset="utf-8">95% in the original post, but the overhead is still clearly there.</p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.adamzolo.com/the-cost-of-postgresql-foreign-keys/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Docker MySQL with a Custom SQL Script for Development</title>
		<link>https://blog.adamzolo.com/docker-mysql-with-a-custom-sql-script-for-development/</link>
					<comments>https://blog.adamzolo.com/docker-mysql-with-a-custom-sql-script-for-development/#respond</comments>
		
		<dc:creator><![CDATA[Adam Zolo]]></dc:creator>
		<pubDate>Tue, 04 Jan 2022 15:07:10 +0000</pubDate>
				<category><![CDATA[docker]]></category>
		<category><![CDATA[SQL]]></category>
		<guid isPermaLink="false">https://blog.adamzolo.com/?p=988</guid>

					<description><![CDATA[The setup is similar to setting up MariaDB. Start with standard docker-compose file. If using custom SQL mode, specify the necessary options in the command options: Add dev.dockerfile: Finally, add your init.sql file. Let&#8217;s give all privileges to our dev_user and switch the default caching_sha2_password to mysql_native_password (don&#8217;t do it unless you rely on older&#8230;<p><a class="more-link" href="https://blog.adamzolo.com/docker-mysql-with-a-custom-sql-script-for-development/" title="Continue reading &#8216;Docker MySQL with a Custom SQL Script for Development&#8217;">Continue reading <span class="meta-nav">&#8594;</span></a></p>]]></description>
										<content:encoded><![CDATA[
<p>The setup is similar to <a rel="noreferrer noopener" href="https://blog.adamzolo.com/dockerizing-mariadb-with-a-custom-sql-script-in-development/" target="_blank">setting up MariaDB</a>.</p>



<p>Start with standard docker-compose file. If using custom SQL mode, specify the necessary options in the command options:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
version: &quot;3.7&quot;
services:
    mysql:
        build:
            context: .
            dockerfile: dev.dockerfile
        restart: always
        command: --sql_mode=&quot;STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE&quot;
        environment:
            MYSQL_ROOT_PASSWORD: root_password
            MYSQL_DATABASE: dev
            MYSQL_USER: dev_user
            MYSQL_PASSWORD: dev_password
        ports:
            - 3306:3306

</pre></div>


<p>Add <code>dev.dockerfile</code>:</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
FROM mysql:8.0.17

ADD init.sql /docker-entrypoint-initdb.d/ddl.sql


</pre></div>


<p><meta charset="utf-8">Finally, add your <code>init.sql</code> file. Let&#8217;s give all privileges to our <code>dev_user</code> and switch the default caching_sha2_password to mysql_native_password (don&#8217;t do it unless you rely on older packages that require the less secure  au</p>



<p>Finally, add your <code>init.sql</code> file. Let&#8217;s give all privileges to our <code>dev_user</code> and switch the default caching_sha2_password to mysql_native_password (don&#8217;t do it unless you rely on older packages that require the less secure <meta charset="utf-8">mysql_native_password authentication method):</p>


<div class="wp-block-syntaxhighlighter-code "><pre class="brush: yaml; title: ; notranslate">
GRANT ALL PRIVILEGES ON *.* TO &#039;dev_user&#039;@&#039;%&#039;;
ALTER USER &#039;dev_user&#039;@&#039;%&#039; IDENTIFIED WITH mysql_native_password BY &#039;dev_password&#039;;
</pre></div>


<p><meta charset="utf-8">If you want to access the database container from other containers, while running them separately, you can specify <code>host.docker.internal</code> as the host address of your database.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.adamzolo.com/docker-mysql-with-a-custom-sql-script-for-development/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
