<?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: Smyekh David-West</title>
    <description>The latest articles on DEV Community by Smyekh David-West (@smyekh).</description>
    <link>https://dev.to/smyekh</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%2F2917118%2Fa91361f4-29fe-4ad2-b7d5-3d64522363cb.jpg</url>
      <title>DEV Community: Smyekh David-West</title>
      <link>https://dev.to/smyekh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/smyekh"/>
    <language>en</language>
    <item>
      <title>How to Actually Use AI to Build Production Software, End to End</title>
      <dc:creator>Smyekh David-West</dc:creator>
      <pubDate>Sat, 18 Apr 2026 08:54:56 +0000</pubDate>
      <link>https://dev.to/smyekh/how-to-actually-use-ai-to-build-production-software-end-to-end-4dg4</link>
      <guid>https://dev.to/smyekh/how-to-actually-use-ai-to-build-production-software-end-to-end-4dg4</guid>
      <description>&lt;p&gt;&lt;em&gt;The stack, the tools, and the step-by-step workflow nobody hands you at the start.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Blueprint
&lt;/h2&gt;

&lt;p&gt;Before you build, here's the full plan. You may bookmark this and come back to whatever stage you're at.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Foundation
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  Before You Write a Line of Code: Use the CLI, Not Just the Browser
&lt;/li&gt;
&lt;li&gt;  First, a Word on Vibecoding vs. Knowing What You're Doing
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Stack
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  1. Database and Cloud Infrastructure: Oracle Cloud (OCI)
&lt;/li&gt;
&lt;li&gt;  2. Hosting and DNS: Cloudflare
&lt;/li&gt;
&lt;li&gt;  3. Backend: FastAPI, Python That Actually Moves
&lt;/li&gt;
&lt;li&gt;  4. Schema Management: Liquibase vs. ORMs
&lt;/li&gt;
&lt;li&gt;  5. Version Control and CI/CD: GitLab
&lt;/li&gt;
&lt;li&gt;  6. Frontend: Start Vanilla, Graduate When Ready
&lt;/li&gt;
&lt;li&gt;  7. Mobile: Flutter and Fastlane
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  The Build
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;  The Bigger Picture: Architecture Is the Job Now
&lt;/li&gt;
&lt;li&gt;  Step-by-Step: From Idea to Production
&lt;/li&gt;
&lt;li&gt;  Stack Summary
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Tools laid out. Time to build.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;There's never been a better time to build software. You can open Claude or Gemini, describe an idea, and watch functional code appear in seconds. But here's the thing nobody tells you in the hype reels: the code is only as good as the decisions behind it.&lt;/p&gt;

&lt;p&gt;With so many tools flooding the market, including Lovable, Bolt, Cursor, and a dozen others, developers and especially newcomers are drowning in choice paralysis. The real question isn't whether AI can write your code. It can. The question is, what stack do you point it at, and how do you use the AI properly in the first place?&lt;/p&gt;

&lt;p&gt;This matters more than most people realise. Mature, well-documented technologies are easier for AI models to work with because they've been trained on years of documentation, Stack Overflow threads, GitHub repos, and real-world usage. When you pick an obscure or newly released framework, you're not just fighting the learning curve yourself. The AI is fighting it too. And that means more hallucinations, more debugging, more time lost.&lt;/p&gt;

&lt;p&gt;So here's a practical, opinionated guide on what to use when you're building something you actually intend to ship and how to use the AI properly while you do it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Before You Write a Line of Code: Use the CLI, Not Just the Browser
&lt;/h2&gt;

&lt;p&gt;Most people start with Claude or Gemini in a browser tab. That's fine for exploring ideas, generating boilerplate, or asking conceptual questions. But once you have an actual project, you're leaving significant capability on the table if you stay in the browser.&lt;/p&gt;

&lt;p&gt;The real power comes from the Claude Code CLI and the Gemini CLI, command-line tools that let the AI agent read your actual codebase, understand the context of your project, and make changes across files with awareness of how everything fits together.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Here's why this matters:&lt;/strong&gt; when you paste code into a browser chat, the AI only sees what you paste. When you use the CLI inside your project, the agent can navigate your file structure, read your existing modules, understand your schema, check your config files, and make decisions based on the whole picture, not just a fragment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on terminals:&lt;/strong&gt; VS Code has a built-in terminal, and it's useful for quick things like creating folders, or running a one-off command. But for your actual AI CLI sessions, download and use &lt;a href="https://www.warp.dev/" rel="noopener noreferrer"&gt;Warp&lt;/a&gt;. Warp is a modern terminal with a genuinely better experience than the default options on Windows or Mac. It has AI features built in, a clean interface, and it handles long-running CLI sessions much more comfortably than the cramped terminal panel inside VS Code. The habit to build is using VS Code for writing and editing code and using Warp for running your Claude Code or Gemini CLI sessions. They complement each other well.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/StMShnRdqnA"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting started with Claude Code:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Make sure you have Node.js installed (version 18 or higher). You can check by running &lt;code&gt;node -v&lt;/code&gt; in your terminal.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install Claude Code globally: &lt;code&gt;npm install -g @anthropic-ai/claude-code&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authenticate with your Anthropic account: &lt;code&gt;claude login&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open Warp, navigate into your project folder: &lt;code&gt;cd your-project-name&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start a session: &lt;code&gt;claude&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Getting started with Gemini CLI:
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Make sure you have Node.js installed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install it: &lt;code&gt;npm install -g @google/gemini-cli&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Authenticate: &lt;code&gt;gemini auth&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open Warp and navigate into your project: &lt;code&gt;cd your-project-name&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Start a session: &lt;code&gt;gemini&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Once you're inside a CLI session in Warp with your project loaded, you can ask the agent things like  &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What does the authentication flow look like in this codebase?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;or &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Add input validation to the user registration endpoint, consistent with the patterns already in this project." &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That level of context-awareness is the difference between AI as a code generator and AI as an actual collaborator.&lt;/p&gt;

&lt;p&gt;Think of the browser as your brainstorming and planning phase. Warp with the CLI is where you build.&lt;/p&gt;

&lt;p&gt;Before you push anything to GitLab, add one more tool to your Warp workflow: CodeRabbit. CodeRabbit is an AI code review tool, and the habit worth building is running it locally against your uncommitted changes before they ever leave your machine.&lt;/p&gt;

&lt;p&gt;Install the &lt;a href="https://www.coderabbit.ai/cli?utm_source=google&amp;amp;utm_medium=cpc&amp;amp;utm_campaign=20944421732&amp;amp;utm_content=158532047035&amp;amp;utm_term=coderabbit&amp;amp;matchtype=e&amp;amp;kw=coderabbit&amp;amp;cpn=20944421732&amp;amp;utm_term=coderabbit&amp;amp;utm_campaign=CodeRabbit+-+Branded&amp;amp;utm_source=adwords&amp;amp;utm_medium=ppc&amp;amp;hsa_acc=9779635742&amp;amp;hsa_cam=20944421732&amp;amp;hsa_grp=158532047035&amp;amp;hsa_ad=697792407004&amp;amp;hsa_src=g&amp;amp;hsa_tgt=aud-2417332813909:kwd-2227824575182&amp;amp;hsa_kw=coderabbit&amp;amp;hsa_mt=e&amp;amp;hsa_net=adwords&amp;amp;hsa_ver=3&amp;amp;gad_source=1&amp;amp;gad_campaignid=20944421732&amp;amp;gbraid=0AAAAAqBNyQXluLRBm1Xeg-rRZ9GeEC5EK&amp;amp;gclid=CjwKCAjwtIfPBhAzEiwAv9RTJt5kIsc2fMtUA6oyaJaHI1dT-eJhNK6WNyQSuwuaA3mKh5jFH_2XahoCN8oQAvD_BwE" rel="noopener noreferrer"&gt;coderabbit CLI&lt;/a&gt; and run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;coderabbit review &lt;span class="nt"&gt;--plain&lt;/span&gt; &lt;span class="nt"&gt;--type&lt;/span&gt; uncommitted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;What this does is review everything you've changed but not yet committed, the same way a senior engineer would if they were looking over your shoulder before you pushed. It flags logic issues, security concerns, inconsistent patterns, missing error handling, and things the AI that wrote the code may not have caught because it was focused on making the feature work rather than making it defensible. Running it before you commit means you're catching problems at the cheapest possible moment, before they're in the pipeline, before they're in a merge request, and before they've touched production.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;--plain&lt;/code&gt; flag keeps the output readable in the terminal rather than formatted for a browser. In Warp, the output renders cleanly and you can scroll through the review the same way you'd read a colleague's notes. If something CodeRabbit flags isn't clear, paste it into your Claude or Gemini CLI session and ask for an explanation or a fix.&lt;/p&gt;

&lt;p&gt;The workflow in Warp then becomes: write code in VS Code, switch to Warp, run &lt;code&gt;coderabbit review --plain --type uncommitted&lt;/code&gt;, address what it surfaces, then commit and push. That sequence gives you two sets of AI eyes on every change before it hits the pipeline: the model that wrote the code and the model reviewing it. They catch different things.&lt;/p&gt;
&lt;h2&gt;
  
  
  First, a Word on Vibecoding vs. Knowing What You're Doing
&lt;/h2&gt;

&lt;p&gt;If you're a pure vibecoder, someone who delegates every architectural decision to the AI, this guide is especially for you. AI will make choices for you, and sometimes those choices are fine. But often, they're fine for a prototype. Not for production.&lt;/p&gt;

&lt;p&gt;I built a smart plant sitter for a hackathon last year using Flet for the frontend. Got it working. Hacked my way through the bugs and was proud of it. But it was limited and not particularly polished by my standards.&lt;/p&gt;


&lt;div class="ltag__link--embedded"&gt;
  &lt;div class="crayons-story "&gt;
  &lt;a href="https://dev.to/smyekh/i-built-a-voice-controlled-plant-sitter-in-python-with-goose-gemini-cli-5g29" class="crayons-story__hidden-navigation-link"&gt;I Built a Voice-Controlled Plant Sitter in Python with Goose &amp;amp; Gemini CLI&lt;/a&gt;


  &lt;div class="crayons-story__body crayons-story__body-full_post"&gt;
    &lt;div class="crayons-story__top"&gt;
      &lt;div class="crayons-story__meta"&gt;
        &lt;div class="crayons-story__author-pic"&gt;

          &lt;a href="/smyekh" class="crayons-avatar  crayons-avatar--l  "&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%2Fuser%2Fprofile_image%2F2917118%2Fa91361f4-29fe-4ad2-b7d5-3d64522363cb.jpg" alt="smyekh profile" class="crayons-avatar__image" width="800" height="1000"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
        &lt;div&gt;
          &lt;div&gt;
            &lt;a href="/smyekh" class="crayons-story__secondary fw-medium m:hidden"&gt;
              Smyekh David-West
            &lt;/a&gt;
            &lt;div class="profile-preview-card relative mb-4 s:mb-0 fw-medium hidden m:inline-block"&gt;
              
                Smyekh David-West
                
              
              &lt;div id="story-author-preview-content-3012327" class="profile-preview-card__content crayons-dropdown branded-7 p-4 pt-0"&gt;
                &lt;div class="gap-4 grid"&gt;
                  &lt;div class="-mt-4"&gt;
                    &lt;a href="/smyekh" class="flex"&gt;
                      &lt;span class="crayons-avatar crayons-avatar--xl mr-2 shrink-0"&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%2Fuser%2Fprofile_image%2F2917118%2Fa91361f4-29fe-4ad2-b7d5-3d64522363cb.jpg" class="crayons-avatar__image" alt="" width="800" height="1000"&gt;
                      &lt;/span&gt;
                      &lt;span class="crayons-link crayons-subtitle-2 mt-5"&gt;Smyekh David-West&lt;/span&gt;
                    &lt;/a&gt;
                  &lt;/div&gt;
                  &lt;div class="print-hidden"&gt;
                    
                      Follow
                    
                  &lt;/div&gt;
                  &lt;div class="author-preview-metadata-container"&gt;&lt;/div&gt;
                &lt;/div&gt;
              &lt;/div&gt;
            &lt;/div&gt;

          &lt;/div&gt;
          &lt;a href="https://dev.to/smyekh/i-built-a-voice-controlled-plant-sitter-in-python-with-goose-gemini-cli-5g29" class="crayons-story__tertiary fs-xs"&gt;&lt;time&gt;Nov 13 '25&lt;/time&gt;&lt;span class="time-ago-indicator-initial-placeholder"&gt;&lt;/span&gt;&lt;/a&gt;
        &lt;/div&gt;
      &lt;/div&gt;

    &lt;/div&gt;

    &lt;div class="crayons-story__indention"&gt;
      &lt;h2 class="crayons-story__title crayons-story__title-full_post"&gt;
        &lt;a href="https://dev.to/smyekh/i-built-a-voice-controlled-plant-sitter-in-python-with-goose-gemini-cli-5g29" id="article-link-3012327"&gt;
          I Built a Voice-Controlled Plant Sitter in Python with Goose &amp;amp; Gemini CLI
        &lt;/a&gt;
      &lt;/h2&gt;
        &lt;div class="crayons-story__tags"&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/ai"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;ai&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/python"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;python&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/opensource"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;opensource&lt;/a&gt;
            &lt;a class="crayons-tag  crayons-tag--monochrome " href="/t/tutorial"&gt;&lt;span class="crayons-tag__prefix"&gt;#&lt;/span&gt;tutorial&lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="crayons-story__bottom"&gt;
        &lt;div class="crayons-story__details"&gt;
          &lt;a href="https://dev.to/smyekh/i-built-a-voice-controlled-plant-sitter-in-python-with-goose-gemini-cli-5g29" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left"&gt;
            &lt;div class="multiple_reactions_aggregate"&gt;
              &lt;span class="multiple_reactions_icons_container"&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/fire-f60e7a582391810302117f987b22a8ef04a2fe0df7e3258a5f49332df1cec71e.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/multi-unicorn-b44d6f8c23cdd00964192bedc38af3e82463978aa611b4365bd33a0f1f4f3e97.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
                  &lt;span class="crayons_icon_container"&gt;
                    &lt;img src="https://assets.dev.to/assets/sparkle-heart-5f9bee3767e18deb1bb725290cb151c25234768a0e9a2bd39370c382d02920cf.svg" width="24" height="24"&gt;
                  &lt;/span&gt;
              &lt;/span&gt;
              &lt;span class="aggregate_reactions_counter"&gt;3&lt;span class="hidden s:inline"&gt; reactions&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/a&gt;
            &lt;a href="https://dev.to/smyekh/i-built-a-voice-controlled-plant-sitter-in-python-with-goose-gemini-cli-5g29#comments" class="crayons-btn crayons-btn--s crayons-btn--ghost crayons-btn--icon-left flex items-center"&gt;
              Comments


              &lt;span class="hidden s:inline"&gt;Add Comment&lt;/span&gt;
            &lt;/a&gt;
        &lt;/div&gt;
        &lt;div class="crayons-story__save"&gt;
          &lt;small class="crayons-story__tertiary fs-xs mr-2"&gt;
            8 min read
          &lt;/small&gt;
            
              &lt;span class="bm-initial"&gt;
                

              &lt;/span&gt;
              &lt;span class="bm-success"&gt;
                

              &lt;/span&gt;
            
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;/div&gt;
&lt;br&gt;
 

&lt;p&gt;Later, I tried to use Flet for a separate production project entirely, and it had so many issues I couldn't hack through them. No amount of debugging was going to get me where I needed to be. The issues weren't bugs in my code; they were limitations in the technology itself. Some frameworks and libraries simply aren't mature enough to live, exist, and thrive in production. I moved to Next.js, and I typically fall back on vanilla JS, HTML, and CSS because basics are important and I'm a proponent of Gall's Law.&lt;/p&gt;

&lt;p&gt;Gall's Law, for those unfamiliar, states that &lt;em&gt;all complex working systems evolved from simpler working systems&lt;/em&gt;. In other words, get the simple version working first, then build complexity on top of it. A framework that skips that foundation tends to crack under real-world pressure.&lt;/p&gt;

&lt;p&gt;The lesson here is that pet projects and production apps are different species. Production demands polish, reliability, and critical decisions made upfront. An idea is no longer enough. Claude and Gemini can generate a hundred groundbreaking ideas before breakfast. What sets you apart is architecture, the decisions about how things fit together, why, and in what order.&lt;/p&gt;

&lt;p&gt;You don't need to know everything. Experienced engineers still Google things constantly. But you do need to know enough to steer. That's what this guide is for.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Database and Cloud Infrastructure: Oracle Cloud (OCI), the Slept-On Giant
&lt;/h2&gt;

&lt;p&gt;Let's start with the one that surprises people: Oracle Cloud Infrastructure, or OCI.&lt;/p&gt;

&lt;p&gt;Most developers reaching for a cloud platform default to AWS, and some go with Google Cloud (GCP) or Azure. These are solid choices, but they come with significant complexity and cost, especially early on. Here's how they compare:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Oracle Cloud (OCI)&lt;/th&gt;
&lt;th&gt;AWS&lt;/th&gt;
&lt;th&gt;GCP&lt;/th&gt;
&lt;th&gt;Azure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Free Tier&lt;/td&gt;
&lt;td&gt;Very generous (Always Free includes Autonomous DB, compute, storage)&lt;/td&gt;
&lt;td&gt;Limited, expires after 12 months&lt;/td&gt;
&lt;td&gt;Limited, some always-free&lt;/td&gt;
&lt;td&gt;Limited, some always-free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database maturity&lt;/td&gt;
&lt;td&gt;Oracle DB has been in production since 1979&lt;/td&gt;
&lt;td&gt;RDS wraps third-party databases&lt;/td&gt;
&lt;td&gt;Cloud SQL/AlloyDB are solid but younger&lt;/td&gt;
&lt;td&gt;Azure SQL is solid, Microsoft-backed&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Steep&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Steep&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI familiarity&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Excellent, most AI training skews AWS&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost at scale&lt;/td&gt;
&lt;td&gt;Competitive, often cheaper&lt;/td&gt;
&lt;td&gt;Expensive if not managed&lt;/td&gt;
&lt;td&gt;Competitive&lt;/td&gt;
&lt;td&gt;Competitive&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Pro Tip:&lt;/strong&gt; In AWS, if your app goes viral and you serve 10 TB of data, you could be looking at a surprise bill of roughly $900. In OCI, that same bill is $0. Architecture is as much about the ledger as it is about the code.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Oracle's database has a legacy that AWS, GCP, and Azure simply can't match on the same terms. It's been battle-tested in some of the most demanding enterprise environments on the planet for over four decades. The Autonomous Database offering on OCI handles a lot of the operational burden for you, including patching, backups, and performance tuning, which is exactly what you want when you're a small team or solo developer trying to ship.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One concrete number worth knowing:&lt;/strong&gt; OCI gives you the first 10 TB of outbound data transfer free every month. AWS starts charging after 100 GB. That is a 100x difference in your egress allowance, and for a startup serving real users, that gap shows up very quickly in your monthly bill.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One thing worth addressing directly:&lt;/strong&gt; you may have seen YouTube videos comparing API performance benchmarks with flashy graphics of requests per second across different database and backend combinations. Those comparisons are often testing systems at a scale you won't touch for a long time, and they rarely reflect the architecture you'd actually be building. Paired with FastAPI on the backend, Oracle's database handles performance very comfortably for the vast majority of real-world production applications. You don't need to let those benchmarks drive your early decisions.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/YnsN52hB8EY"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The OCI interface is surprisingly clean. If you get stuck, screenshot the console and paste it into Claude or Gemini to walk you through it step by step. It works.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One more thing worth saying upfront:&lt;/strong&gt; the right way to set up OCI is not to click through the console and configure things manually. That approach works for a first look, but it doesn't scale, it doesn't reproduce cleanly across environments, and it leaves your security configuration undocumented and easy to get wrong. The better approach is to provision everything with &lt;strong&gt;Terraform&lt;/strong&gt; from the start.&lt;/p&gt;

&lt;p&gt;Terraform is an infrastructure-as-code tool that lets you define your entire cloud environment in configuration files, what compute to spin up, what networking rules to apply, which IAM policies to attach, which secrets to store in Vault, and what security lists to enforce. You write it once, apply it, and your infrastructure exists exactly as described. If you need a staging environment, you apply the same configuration with different variables. If something breaks, you have a complete record of what was provisioned and why.&lt;/p&gt;

&lt;p&gt;Starting with Terraform on OCI means your security lists, IAM policies, and Vault secrets are codified alongside your application code, version-controlled in GitLab, and reviewable. That's a significantly more defensible position than manually clicking through the OCI console and hoping you remember what you configured six months later. The AI can write your Terraform configuration for you. Give it your architecture and ask it to scaffold the OCI provider setup, and you'll have a working starting point within minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Hosting and DNS: Cloudflare, Far More Than a Domain Registrar
&lt;/h2&gt;

&lt;p&gt;A lot of people's first instinct for domain registration and hosting is GoDaddy. It's heavily marketed and familiar. But once you go with Cloudflare, it's genuinely hard to go back.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Cloudflare&lt;/th&gt;
&lt;th&gt;GoDaddy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Primary strength&lt;/td&gt;
&lt;td&gt;CDN, DDoS protection, edge hosting, DNS&lt;/td&gt;
&lt;td&gt;Domain registration, basic hosting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Performance&lt;/td&gt;
&lt;td&gt;Global CDN across 330+ cities worldwide&lt;/td&gt;
&lt;td&gt;Standard hosting from a single region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Security&lt;/td&gt;
&lt;td&gt;Built-in DDoS protection, WAF, SSL&lt;/td&gt;
&lt;td&gt;Paid add-ons for most security features&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Developer tools&lt;/td&gt;
&lt;td&gt;Workers, Pages, R2, KV, D1, Wrangler CLI&lt;/td&gt;
&lt;td&gt;Limited developer-facing tooling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;Generous, Pages, Workers, and CDN free to start&lt;/td&gt;
&lt;td&gt;Minimal&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI familiarity&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Basic&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;When Cloudflare says "edge," it means your site is served from whichever of their 330+ city locations is closest to the person loading it. When GoDaddy hosts your site, it's served from one place. If that one data centre is in Dallas and your user is in Lagos, they're waiting for the full round trip. With Cloudflare, a user in Lagos is likely hitting a nearby node. That's not a small difference in practice.&lt;/p&gt;

&lt;p&gt;Cloudflare Pages lets you deploy static frontends and full-stack apps directly from a GitHub or GitLab repo. Cloudflare Workers lets you run serverless backend logic at the edge, meaning it executes close to your user's location, reducing latency significantly. Cloudflare R2 is object storage without egress fees, which is a major and often invisible cost with AWS S3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A word on Wrangler:&lt;/strong&gt; Cloudflare's command-line tool is called Wrangler, and it's how you develop, test, and deploy Cloudflare Workers and Pages locally before pushing them live. Instead of deploying blind, you run &lt;code&gt;wrangler dev&lt;/code&gt; to simulate the edge environment on your own machine. Claude and Gemini both know Wrangler's syntax well, so you can ask the AI to write or debug your &lt;code&gt;wrangler.toml&lt;/code&gt; configuration, generate Worker scripts, or set up bindings to R2 storage and KV namespaces. Run Wrangler from Warp for a cleaner experience, the same way you'd run your AI CLI sessions.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/aDnQKqwuzuw"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;For any production project, you want your site behind Cloudflare regardless. The DDoS protection alone is worth it, and the free SSL certificates mean you're not leaving security to chance.&lt;/p&gt;

&lt;p&gt;GoDaddy is fine for buying a domain. That's about where its utility ends for serious development.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Backend: FastAPI, Python That Actually Moves
&lt;/h2&gt;

&lt;p&gt;Backend choice is where a lot of decisions get religious. Let's cut through it.&lt;/p&gt;

&lt;p&gt;FastAPI is a Python web framework built specifically for building APIs quickly and cleanly. It comes with automatic request validation, data serialisation, and interactive API documentation generated from your code, all out of the box.&lt;/p&gt;

&lt;p&gt;Here's how it compares to the common alternatives:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;FastAPI&lt;/th&gt;
&lt;th&gt;Node.js / Express&lt;/th&gt;
&lt;th&gt;.NET (C#)&lt;/th&gt;
&lt;th&gt;Spring Boot (Java)&lt;/th&gt;
&lt;th&gt;Laravel (PHP)&lt;/th&gt;
&lt;th&gt;Ruby on Rails&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Language&lt;/td&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;JavaScript&lt;/td&gt;
&lt;td&gt;C#&lt;/td&gt;
&lt;td&gt;Java&lt;/td&gt;
&lt;td&gt;PHP&lt;/td&gt;
&lt;td&gt;Ruby&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Speed&lt;/td&gt;
&lt;td&gt;Very fast (async-first)&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Very fast&lt;/td&gt;
&lt;td&gt;Fast&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Low-moderate&lt;/td&gt;
&lt;td&gt;Low-moderate&lt;/td&gt;
&lt;td&gt;Steep&lt;/td&gt;
&lt;td&gt;Steep&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Type safety&lt;/td&gt;
&lt;td&gt;Built-in via Pydantic&lt;/td&gt;
&lt;td&gt;Optional via TypeScript&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Auto API docs&lt;/td&gt;
&lt;td&gt;Yes (Swagger + ReDoc)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Partial (Swagger add-on)&lt;/td&gt;
&lt;td&gt;Partial (Swagger add-on)&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI familiarity&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verbosity&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;td&gt;Very high&lt;/td&gt;
&lt;td&gt;Moderate&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;APIs, microservices, AI-adjacent services&lt;/td&gt;
&lt;td&gt;Full-stack JS apps, real-time&lt;/td&gt;
&lt;td&gt;Enterprise systems, Windows ecosystems&lt;/td&gt;
&lt;td&gt;Large enterprise backends&lt;/td&gt;
&lt;td&gt;Content-heavy web apps, rapid prototyping&lt;/td&gt;
&lt;td&gt;Full-stack web apps, startups&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A few notes on the alternatives worth calling out specifically:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;.NET (C#)&lt;/strong&gt; is a serious, high-performance framework backed by Microsoft. It's genuinely excellent for enterprise environments, especially where the rest of the organisation is already running Windows infrastructure. But it carries real weight. The ecosystem is verbose, the setup is heavier, and if you're building as a solo developer or small team, you'll spend more time on configuration than on your actual product. The AI can write C# competently, but the debugging surface is larger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Spring Boot (Java)&lt;/strong&gt; is one of the most widely deployed backend frameworks in the world, particularly in large enterprises and financial institutions. If you've ever applied for a job and seen "5 years Spring Boot experience required," this is why. It is genuinely battle-hardened. But it is also genuinely complex. Annotations, dependency injection, and the sheer volume of configuration it expects from you make it a steep climb for anyone not already comfortable in the Java ecosystem. For a solo developer building with AI assistance, the verbosity alone makes debugging slower.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node.js with Express&lt;/strong&gt; is a natural choice if your frontend is already in JavaScript and you want to stay in one language end-to-end. The AI handles it well and the ecosystem is enormous. The trade-off is that JavaScript's loose typing means errors that Python would catch at definition time often only surface at runtime. TypeScript helps, but it adds a build step and its own complexity. For teams already living in JavaScript, Express or its more opinionated cousin Fastify makes sense. If you're choosing a language from scratch, Python's readability and FastAPI's structure give you a cleaner starting point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Laravel (PHP)&lt;/strong&gt; is worth more respect than it typically gets in developer discourse. Modern PHP is not the PHP of 2008. Laravel ships with a clean ORM, authentication scaffolding, a task queue, and good documentation. It has a large community and deploys easily to almost any shared hosting environment, which matters when you're early and cost-conscious. The honest limitation is that the AI's training data for Laravel is thinner than for FastAPI or Express, which means you'll hit more friction when the generated code needs debugging. It remains a solid choice if you already know PHP.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ruby on Rails&lt;/strong&gt; pioneered a lot of what we now consider standard in web frameworks: convention over configuration, built-in ORM, database migrations as code, and scaffolding. It's still used in production by serious companies. The challenge today is momentum. The Rails community has shrunk relative to its peak, which means fewer recent training examples for the AI and a smaller pool of developers to hire from if your project grows. For solo projects and quick prototypes it remains genuinely pleasant to use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;FastAPI&lt;/strong&gt; wins for this stack for a specific combination of reasons: Python's readability makes it easier to evaluate AI-generated code rather than just accepting it, the async-first design handles real-world I/O efficiently, the automatic documentation means your API is self-describing from day one, and the AI models are exceptionally well-trained on FastAPI patterns. When something goes wrong, the error messages are clear and the debugging path is short. That combination matters more than benchmark scores when you're shipping something real.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Go (Golang)&lt;/strong&gt; deserves an honourable mention. It is genuinely fast, its concurrency model is elegant once you understand it, and production Go services tend to be lean and reliable. But you have to learn it first. Go's error handling, interface system, and approach to concurrency are different enough from most languages that you can't just point the AI at a problem and trust the output without understanding it. Once you have the foundation, it's a strong choice for high-throughput services. Without it, you're flying blind when something breaks unexpectedly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The core point stands throughout:&lt;/strong&gt; mature frameworks are easier for AI to work with because the models have been trained on years of real-world usage. FastAPI paired with Oracle DB handles production traffic comfortably for the kind of applications most developers are actually building. The YouTube benchmark videos are testing someone else's scale, not yours.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/umbU5Pk03CM"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Schema Management: Liquibase vs. ORMs, and Why the Distinction Matters
&lt;/h2&gt;

&lt;p&gt;This one trips people up, so let's define the terms first.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is an ORM?
&lt;/h3&gt;

&lt;p&gt;ORM stands for Object-Relational Mapper. It's a layer of code that lets you interact with your database using the programming language you're already writing in, rather than writing raw SQL. SQL, or Structured Query Language, is the language databases speak natively. Instead of writing &lt;code&gt;SELECT * FROM users WHERE id = 1&lt;/code&gt; directly in SQL, an ORM lets you write something like &lt;code&gt;User.query.get(1)&lt;/code&gt; in Python, and it translates that into SQL behind the scenes.&lt;/p&gt;

&lt;p&gt;Popular ORMs include SQLAlchemy (Python), Prisma (Node.js), Hibernate (Java), and Django's built-in ORM. They feel natural to start with, especially when AI is generating the code, because they keep everything in one language. The problem is that they abstract away too much of the database. In production, you'll eventually hit a situation where the ORM's migration logic conflicts with your actual schema, or where you need precise control over how a change is applied across environments like development, staging, and production.&lt;/p&gt;

&lt;p&gt;That's where Liquibase earns its place.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Liquibase&lt;/th&gt;
&lt;th&gt;ORM Migrations (SQLAlchemy, Prisma, etc.)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Control&lt;/td&gt;
&lt;td&gt;Full control over every schema change&lt;/td&gt;
&lt;td&gt;High-level abstraction, less manual control&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Database-agnostic&lt;/td&gt;
&lt;td&gt;Yes, works across Oracle, Postgres, MySQL, etc.&lt;/td&gt;
&lt;td&gt;Often framework/language-specific&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rollback support&lt;/td&gt;
&lt;td&gt;Built-in, explicit rollback scripts&lt;/td&gt;
&lt;td&gt;Varies, often manual and error-prone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit trail&lt;/td&gt;
&lt;td&gt;Yes, changelog tracks every change ever made&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Learning curve&lt;/td&gt;
&lt;td&gt;Moderate (free courses and certification available)&lt;/td&gt;
&lt;td&gt;Low initially, painful at scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI familiarity&lt;/td&gt;
&lt;td&gt;Good, Claude and Gemini understand changesets and changelogs well&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Liquibase uses changelogs and changesets, a versioned history of every change ever made to your database schema. You can roll forward, roll back, and audit exactly what changed, when, and why. In a production environment, this is invaluable.&lt;/p&gt;

&lt;p&gt;The courses are free on Liquibase's website and you can get certified, which is a genuine bonus. &lt;/p&gt;

&lt;p&gt;It also means you can steer the AI confidently when it generates migration scripts, because Claude and Gemini both handle Liquibase XML, YAML, and SQL changelogs reliably.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/53bieWfNnFQ"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Oracle DB vs. PostgreSQL vs. MongoDB:
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Oracle DB&lt;/th&gt;
&lt;th&gt;PostgreSQL&lt;/th&gt;
&lt;th&gt;MongoDB&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Type&lt;/td&gt;
&lt;td&gt;Relational (SQL)&lt;/td&gt;
&lt;td&gt;Relational (SQL)&lt;/td&gt;
&lt;td&gt;Document (NoSQL)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maturity&lt;/td&gt;
&lt;td&gt;45+ years&lt;/td&gt;
&lt;td&gt;30+ years&lt;/td&gt;
&lt;td&gt;~15 years&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best for&lt;/td&gt;
&lt;td&gt;Enterprise, complex transactions&lt;/td&gt;
&lt;td&gt;General purpose, advanced queries&lt;/td&gt;
&lt;td&gt;Flexible schema, rapid prototyping&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ACID compliance&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Full&lt;/td&gt;
&lt;td&gt;Partial&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cost&lt;/td&gt;
&lt;td&gt;Free on OCI always-free tier&lt;/td&gt;
&lt;td&gt;Free (open source)&lt;/td&gt;
&lt;td&gt;Free tier, paid tiers on Atlas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;AI familiarity&lt;/td&gt;
&lt;td&gt;Good&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;td&gt;Excellent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;A quick explanation on ACID:&lt;/strong&gt;  ACID stands for Atomic, Consistent, Isolated, and Durable. It's the set of guarantees a database makes about your transactions. In plain terms, a transaction either fully completes or it doesn't happen at all.&lt;/p&gt;

&lt;p&gt;Here's why this matters in practice: imagine you're transferring money from a Savings table to a Checking table. You deduct from Savings, then add to Checking. If the power cuts out between those two steps, what happens to the money? With a fully ACID-compliant database, it doesn't vanish. The whole transaction either lands or it rolls back. It's either in Savings or in Checking. There's no in-between state where it's nowhere. No vibes allowed in the ledger. For anything involving money, user accounts, orders, or sensitive records, this guarantee is non-negotiable.&lt;/p&gt;

&lt;p&gt;PostgreSQL is the community favourite for relational databases and genuinely excellent. But if you're already on Oracle Cloud, it's worth knowing that Oracle 23ai introduces native AI Vector Search. This means you don't need a separate vector database like Pinecone to build AI-powered search or recommendation features. You can keep your relational data and your AI embeddings in the same database. For anyone building AI-assisted features into their product, that's a meaningful simplification to your stack.&lt;/p&gt;

&lt;p&gt;MongoDB's schema-less nature is appealing for prototypes because you don't have to define your structure upfront. But in production, that same flexibility becomes a liability when your data is inconsistent and your queries are slow. For any serious transactional application, go relational.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/SzWpcq1DzCU"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Version Control and CI/CD: GitLab Is Doing More Than You Think
&lt;/h2&gt;

&lt;p&gt;Most people reach for GitHub by default, and it's a solid choice. But GitLab, especially if you're building a serious project, deserves a closer look, primarily because of how deeply integrated its CI/CD tooling is.&lt;/p&gt;

&lt;p&gt;CI/CD stands for Continuous Integration and Continuous Deployment. In plain terms: every time you push code, a pipeline automatically runs, testing it, building it, and deploying it, so you're not doing those steps manually every single time. Once it's set up, it works in the background and you just make edits.&lt;/p&gt;

&lt;p&gt;GitLab manages all of this through a single file in your repository called &lt;code&gt;.gitlab-ci.yml&lt;/code&gt;. This file defines your pipeline stages, what runs, when, and in what order. And here's where AI becomes genuinely useful: Claude and Gemini can write these files for you.&lt;/p&gt;

&lt;p&gt;Open Warp, navigate into your project, start a CLI session, and ask: "Write a GitLab CI/CD pipeline for this FastAPI project that runs tests, builds a Docker image, and deploys to OCI". You'll get a working draft. You can also paste in a failing pipeline log and ask the AI to diagnose it. The full version with Docker image building and registry pushes is covered in Step 4 of the walkthrough.&lt;/p&gt;

&lt;p&gt;A basic &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; for a FastAPI project might look like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;

&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python:3.11&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pip install -r requirements.txt&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pytest&lt;/span&gt;

&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker build -t my-app .&lt;/span&gt;

&lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./deploy.sh&lt;/span&gt;
  &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Beyond the local review habit, CodeRabbit integrates directly with GitLab and can be configured to automatically review every merge request your team opens. Once connected, it posts a structured review as a comment on the MR, covering the same ground as the local review but scoped to the diff between your branch and main. For solo developers it's a useful second pass. For small teams it functions as an always-available reviewer who has read the entire codebase and never gets tired.&lt;/p&gt;

&lt;p&gt;The combination worth aiming for is: local &lt;code&gt;coderabbit review --plain --type uncommitted&lt;/code&gt; before you commit, GitLab MR review automatically when you push a branch, and the pipeline running tests and deployment after the review. By the time code reaches your production branch it has been looked at by the model that wrote it, the model that reviewed it locally, CodeRabbit on the MR, and your test suite. That is a meaningfully more robust process than most small teams run, and none of it requires a dedicated QA engineer.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/twbp_auLOcg"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;The key habit to build: when a pipeline fails, copy the logs from GitLab and paste them directly as a prompt into Claude or Gemini. Don't try to debug from memory. The AI will read the exact error and tell you what went wrong. This alone saves hours.&lt;/p&gt;

&lt;p&gt;GitLab also has a clean interface, project management tooling, and a container registry built in, meaning you're not cobbling together five different services to manage your project.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Frontend: Start Vanilla, Graduate When Ready
&lt;/h2&gt;

&lt;p&gt;Here's my standing recommendation: vanilla HTML, CSS, and JavaScript first.&lt;/p&gt;

&lt;p&gt;Not because frameworks are bad. React, Next.js, Vue, these are all excellent. But the fundamentals matter. Gall's Law applies here too. When you understand what a framework is abstracting, you become dramatically better at using it and debugging it.&lt;/p&gt;

&lt;p&gt;For production, the progression looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Use Case&lt;/th&gt;
&lt;th&gt;Recommendation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Static sites, simple pages&lt;/td&gt;
&lt;td&gt;Vanilla HTML/CSS/JS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Content-heavy sites, SEO-critical&lt;/td&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interactive dashboards&lt;/td&gt;
&lt;td&gt;React&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Full-stack with server-side rendering&lt;/td&gt;
&lt;td&gt;Next.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rapid prototyping&lt;/td&gt;
&lt;td&gt;Any, keep it simple&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The advantage of vanilla is zero build tooling, zero dependency conflicts, and full transparency about what your code is doing. The advantage of Next.js is server-side rendering, file-based routing, and first-class deployment on Vercel or Cloudflare Pages.&lt;/p&gt;

&lt;p&gt;When you use AI to build a Next.js app, you'll hit some debugging. That's fine. Push through it and you'll understand the framework better than if it had just worked the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Mobile: Flutter and Fastlane, With Realistic Expectations
&lt;/h2&gt;

&lt;p&gt;For mobile app development, Flutter deserves its spot on this list. It's Google's cross-platform framework using the Dart language, and it lets you build iOS and Android apps from a single codebase. Claude and Gemini can generate significant amounts of working Flutter code with clear prompts. The widget system is well-documented and AI familiarity with it is solid.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The honest truth:&lt;/strong&gt; building the app is the easy part. Getting it through Apple's App Store review process and Google Play's requirements is its own discipline. There are real hoops, developer accounts, code signing certificates, privacy policies, age ratings, and binary review rounds.&lt;/p&gt;

&lt;p&gt;This is where &lt;strong&gt;Fastlane&lt;/strong&gt; earns its place. Fastlane is an open-source automation tool specifically built for mobile app deployment. It automates the painful parts: building your app, running tests, managing code signing, taking screenshots, and submitting to the App Store or Play Store. You define your deployment workflow in a &lt;code&gt;Fastfile&lt;/code&gt;, and Fastlane executes it.&lt;/p&gt;

&lt;p&gt;Claude and Gemini both understand Fastlane's configuration structure well. You can ask the AI to generate a &lt;code&gt;Fastfile&lt;/code&gt; for your Flutter project that handles both iOS and Android submission, then iterate from there. Combined with a GitLab CI/CD pipeline, you can set up a workflow where a push to your &lt;code&gt;main&lt;/code&gt; branch automatically triggers a Fastlane deployment to both stores. Your release process becomes automated and repeatable rather than a manual scramble every time.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/CwzAWVgJeK8"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The Bigger Picture: Architecture Is the Job Now
&lt;/h2&gt;

&lt;p&gt;Here's what I keep coming back to: ideas are cheap now. Ask Claude or Gemini for startup ideas, feature ideas, marketing angles and you'll have a hundred in an hour. The bottleneck has shifted entirely to implementation and, more specifically, to architecture.&lt;/p&gt;

&lt;p&gt;Architecture means knowing which database fits your data model, knowing how your frontend and backend will communicate, knowing where your bottlenecks will be before you hit them, knowing how to manage schema changes without breaking production, and knowing how your code gets from your laptop to a live server.&lt;/p&gt;

&lt;p&gt;You don't need to know everything before you start. You need to know enough to make good decisions and recognise bad ones. The rest you learn as you go, and the AI helps fill the gaps when you have the baseline to evaluate what it's telling you.&lt;/p&gt;

&lt;p&gt;Don't chase the $1M ARR, 24k-user story without asking yourself: if I wanted to build that, where would I actually start? Because the answer requires architecture. It requires picking a stack, understanding why, and knowing how to steer when things go sideways.&lt;/p&gt;




&lt;h2&gt;
  
  
  Step-by-Step: From Idea to Production
&lt;/h2&gt;

&lt;p&gt;This is the part most guides skip. Here's how to actually start and finish.&lt;/p&gt;

&lt;h3&gt;
  
  
   Step 1: Open VSCode and Start in the Browser
&lt;/h3&gt;

&lt;p&gt;Download and open Visual Studio Code from code.visualstudio.com. It's free and the most widely supported editor for AI-assisted development.&lt;/p&gt;

&lt;p&gt;Before touching any terminal, start in the browser with Claude (claude.ai) or Gemini (gemini.google.com). Use this phase to describe your project, plan the architecture, generate your initial project structure and boilerplate, and decide on your folder layout before you write a single file.&lt;/p&gt;

&lt;p&gt;If you've never done this before, just say so. Tell the AI: &lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"I want to build [your idea]. I'm new to this. What should my project structure look like, what commands do I need to run to set it up, and what should I do first?" &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It will walk you through it step by step and tell you what to install if you're missing anything.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Move to Warp for CLI Sessions
&lt;/h3&gt;

&lt;p&gt;Download Warp from &lt;a href="https://www.warp.dev/" rel="noopener noreferrer"&gt;warp.dev&lt;/a&gt; and open it alongside VS Code. This is your AI CLI environment.&lt;/p&gt;

&lt;p&gt;In Warp, navigate into your project folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;your-project-name
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start a Claude Code or Gemini CLI session from here. From this point, the AI can see your entire codebase. Ask it context-aware questions like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;"What's the best way to add authentication to this project based on what's already here?"&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"Review my current folder structure and suggest improvements."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;"Write tests for the endpoints in users.py."&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Keep VS Code open for editing and writing code, and use its built-in Source Control sidebar for staging changes and writing commit messages. Use Warp for AI CLI sessions, running builds, CodeRabbit reviews, and anything that benefits from a full-screen terminal. They work together; use each for what it's best at.&lt;/p&gt;

&lt;p&gt;One Warp feature worth knowing about specifically is its tab panel. Warp lets you open multiple tabs in the same window and switch between them instantly, and once your project has a few moving parts this becomes genuinely useful. A practical setup is one tab per concern: one for your Claude or Gemini CLI session, one for running the Terraform workflow, one for your GitLab pipeline commands and log watching, one for CodeRabbit reviews, and one for general project commands like starting your FastAPI server locally or running tests. Everything lives in one window and you switch between contexts without losing your place in any of them.&lt;/p&gt;

&lt;p&gt;This is a small thing that compounds over a full working session. The alternative is constantly interrupting your AI CLI session to run a different command, or juggling multiple terminal windows. The tab setup keeps each workflow isolated and visible.&lt;/p&gt;

&lt;p&gt;Before you commit anything, make it a habit to run CodeRabbit in Warp first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;coderabbit review &lt;span class="nt"&gt;--plain&lt;/span&gt; &lt;span class="nt"&gt;--type&lt;/span&gt; uncommitted
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read what it surfaces, address the issues worth fixing, and then commit. This takes two minutes and consistently catches things that both you and the AI that generated the code missed. It is the easiest quality gate you can add to your workflow because it requires no configuration and runs entirely locally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Structure Your Backend as a Modular Monolith
&lt;/h3&gt;

&lt;p&gt;When your AI starts generating backend code, guide it toward a modular monolith structure. This is an architecture pattern where your application lives in a single deployable unit, one backend service, but is organised internally into distinct modules, one per feature.&lt;/p&gt;

&lt;p&gt;Each module contains three files. &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;schema.py&lt;/code&gt; defines the data structures, what the data looks like. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;service.py&lt;/code&gt; contains the business logic, what the application does with the data. &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;controller.py&lt;/code&gt; handles the API endpoints, how the outside world interacts with the feature.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A project structure should follow this template:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/
├── users/
│   ├── users_schema.py
│   ├── users_service.py
│   └── users_controller.py
├── products/
│   ├── products_schema.py
│   ├── products_service.py
│   └── products_controller.py
├── orders/
│   ├── orders_schema.py
│   ├── orders_service.py
│   └── orders_controller.py
└── main.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;You’ll notice I use explicit names like &lt;code&gt;users_schema.py&lt;/code&gt; instead of just &lt;code&gt;schema.py&lt;/code&gt;. While traditional architecture often favors generic names inside a folder, explicit naming is an "AI-First" strategy. When you are working with an AI agent across multiple files, generic names like &lt;code&gt;schema.py&lt;/code&gt; can lead to "context drift" where the AI confuses the User schema with the Product schema. By being explicit, you ensure that every file carries its own identity, making it easier for you and the AI to single out exactly what needs to change without ambiguity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This structure does several things for you. Features are easy to identify and isolate. Debugging is faster because you know exactly which file to look in for any given problem. AI collaboration is cleaner because you can point the agent at a specific module without it touching unrelated code. And when your project grows, it gives you a blueprint to extract a module into its own microservice if you ever need to.&lt;/p&gt;

&lt;p&gt;Tell the AI explicitly: "When generating backend code for this project, use a modular monolith structure with schema, service, and controller files per feature."&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Containerise Everything with Docker and Docker Compose
&lt;/h3&gt;

&lt;p&gt;Once your backend structure is in place, the next question is how it actually runs, both on your machine during development and on the OCI VM in production. The answer is Docker, and for orchestrating multiple services together, Docker Compose.&lt;/p&gt;

&lt;p&gt;Docker packages your application and everything it needs to run into a container. A container is a self-contained, isolated environment that behaves the same way on your laptop, on a teammate's machine, and on a server in OCI's data centre. No more "it works on my machine." If it runs in the container, it runs everywhere the container runs.&lt;/p&gt;

&lt;p&gt;Docker Compose takes this further by letting you define and run multiple containers together using a single YAML file called &lt;code&gt;docker-compose.yml&lt;/code&gt;. This is where it becomes genuinely useful for a modular monolith: instead of running one giant process, each concern gets its own container on the same VM. Your FastAPI backend runs in one container. Your Next.js frontend runs in another. Liquibase runs its migrations in another. A reverse proxy like Caddy or Nginx sits in front of all of them and routes incoming traffic to the right place.&lt;/p&gt;

&lt;p&gt;This is not Kubernetes. It does not require a cluster, a cloud-native team, or a certification to operate. It is one VM, several containers, one Compose file. And given the compute and storage OCI provides on its always-free tier and low-cost instances, a single VM handles this comfortably for most production workloads.&lt;/p&gt;

&lt;p&gt;It is also the building block you need before Kubernetes ever makes sense. Every concept you learn here, images, containers, networking between services, health checks, environment variables, you carry directly into Kubernetes when the time comes. But that time is not now. Start here.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/Gjnup-PuquQ"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;docker-compose.yml&lt;/code&gt; for a typical project on this stack looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;caddy:alpine&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./Caddyfile:/etc/caddy/Caddyfile&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;caddy_data:/data&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;frontend&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;caddy"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;version"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-app-backend&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./backend&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8000"&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:8000/health"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

  &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-app-frontend&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./frontend&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000"&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

  &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-app-worker&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./worker&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;

  &lt;span class="na"&gt;liquibase&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;liquibase/liquibase&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./liquibase:/liquibase/changelog&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="s"&gt;--changelog-file=changelog/db.changelog-root.yaml&lt;/span&gt;
      &lt;span class="s"&gt;--url=${DB_URL}&lt;/span&gt;
      &lt;span class="s"&gt;--username=${DB_USER}&lt;/span&gt;
      &lt;span class="s"&gt;--password=${DB_PASSWORD}&lt;/span&gt;
      &lt;span class="s"&gt;update&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:alpine&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;6379"&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;caddy_data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth noting in this file:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caddy&lt;/strong&gt; handles HTTPS automatically. Point your domain at the VM, configure a &lt;code&gt;Caddyfile&lt;/code&gt; with your domain name, and Caddy requests and renews TLS certificates from Let's Encrypt without any manual steps. Nginx works equally well and the AI knows both, but Caddy requires significantly less configuration to get HTTPS working correctly. Ask the AI to generate a &lt;code&gt;Caddyfile&lt;/code&gt; that routes your domain to your backend and frontend containers and it will produce a working starting point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;expose&lt;/code&gt; keyword&lt;/strong&gt; makes a port available only between containers on the same internal network. The &lt;code&gt;ports&lt;/code&gt; keyword maps a container port to the host machine. Your backend and frontend use &lt;code&gt;expose&lt;/code&gt; because they should never be directly reachable from the internet. Only Caddy uses &lt;code&gt;ports&lt;/code&gt; because it is the only thing that should be.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Liquibase&lt;/strong&gt; runs as a container too. It connects to your Oracle database, applies any pending changesets, and exits. In production you run it as part of your deployment pipeline before the backend starts. The AI can generate both the Liquibase container configuration and the migration files based on your Pydantic schemas.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;.env&lt;/code&gt; file&lt;/strong&gt; holds your database credentials, API keys, and anything else that should never be committed. These values are injected into the containers at runtime. On the OCI VM, they come from OCI Vault via your startup scripts or a secrets management integration. Locally, they live in a .env file that is listed in your &lt;code&gt;.gitignore&lt;/code&gt;. Deciding what belongs in your pipeline's CI/CD variables versus what belongs in OCI Vault is worth a moment's thought, and the AI can help you make that call — more on this in the CI/CD step.&lt;/p&gt;

&lt;p&gt;When you SSH into your VM and run &lt;code&gt;docker ps&lt;/code&gt;, a healthy setup looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;CONTAINER ID   IMAGE                  COMMAND                  CREATED        STATUS                  PORTS                                                         NAMES
a1b2c3d4e5f6   your-app-backend       "uv run uvicorn app…"    2 days ago     Up 2 days (healthy)     8000/tcp                                                      app_backend
b2c3d4e5f6a7   your-app-frontend      "docker-entrypoint.s…"   2 days ago     Up 2 days (healthy)     3000/tcp                                                      app_frontend
c3d4e5f6a7b8   your-app-worker        "uv run arq app.wor…"    2 days ago     Up 2 days                                                                             app_worker
&lt;/span&gt;&lt;span class="gp"&gt;d4e5f6a7b8c9   caddy:alpine           "caddy run --config…"    2 days ago     Up 2 days (healthy)     0.0.0.0:80-&amp;gt;&lt;/span&gt;80/tcp, 0.0.0.0:443-&amp;gt;443/tcp, 443/udp            caddy_proxy
&lt;span class="go"&gt;e5f6a7b8c9d0   redis:alpine           "docker-entrypoint.s…"   5 weeks ago    Up 5 days (healthy)     6379/tcp                                                      app_redis
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every service running, every health check green, Caddy the only thing exposed to the internet. That is what a clean deployment looks like.&lt;/p&gt;

&lt;h4&gt;
  
  
  How this connects to your GitLab pipeline
&lt;/h4&gt;

&lt;p&gt;Your &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; builds the Docker images, pushes them to GitLab's built-in container registry, and then triggers a deployment on the OCI VM. A deploy stage for this setup looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;

&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;python:3.11&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pip install -r backend/requirements.txt&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pytest backend/&lt;/span&gt;

&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;build&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker build -t registry.gitlab.com/yourusername/your-project/backend:$CI_COMMIT_SHA ./backend&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker build -t registry.gitlab.com/yourusername/your-project/frontend:$CI_COMMIT_SHA ./frontend&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker push registry.gitlab.com/yourusername/your-project/backend:$CI_COMMIT_SHA&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker push registry.gitlab.com/yourusername/your-project/frontend:$CI_COMMIT_SHA&lt;/span&gt;
  &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;ssh ubuntu@your-oci-vm "cd /app &amp;amp;&amp;amp; docker compose pull &amp;amp;&amp;amp; docker compose up -d"&lt;/span&gt;
  &lt;span class="na"&gt;only&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;$CI_COMMIT_SHA&lt;/code&gt; tags each image with the exact commit that produced it. This means you always know which version of the code is running in each container, and rolling back is as simple as pulling an earlier tag and restarting. The deploy stage SSHes into the VM, pulls the newly built images, and restarts the relevant containers with zero downtime from the other running services.&lt;/p&gt;

&lt;p&gt;When something goes wrong during a deployment, the logs tell you exactly which container failed and why. Copy them from GitLab and paste them as a prompt. The AI will diagnose whether it is a build error, a misconfigured environment variable, a failed health check, or a networking issue between containers, and tell you what to change.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: Write Your Schemas in Python, Manage the Database with Liquibase
&lt;/h3&gt;

&lt;p&gt;Define your data models in &lt;code&gt;schema.py&lt;/code&gt; using Pydantic, which FastAPI uses natively. These Python classes describe what your data looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pydantic&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BaseModel&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserCreate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BaseModel&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the actual database schema, the tables and columns in Oracle, use Liquibase changesets. Ask the AI to generate them based on your Pydantic models: "Based on these Pydantic schemas, write Liquibase changesets in YAML format to create the corresponding database tables."&lt;/p&gt;

&lt;p&gt;Your Liquibase changelog keeps a versioned record of every schema change. Every time you add a column, rename a table, or add an index, it goes through a new changeset. This gives you full rollback capability and a clean audit trail.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 6: Provision Oracle Cloud with Terraform
&lt;/h3&gt;

&lt;p&gt;Create a free account at &lt;a href="https://cloud.oracle.com/" rel="noopener noreferrer"&gt;cloud.oracle.com&lt;/a&gt;. The Always Free tier gives you an Autonomous Database, compute instances, and object storage.&lt;/p&gt;

&lt;p&gt;Before you touch the OCI console to configure anything meaningful, write your infrastructure as code. This is not the advanced step it might sound like. It is the right starting point, and the AI will help you get there.&lt;/p&gt;

&lt;p&gt;Terraform works by reading &lt;code&gt;.tf&lt;/code&gt; configuration files and applying them against your cloud provider. For OCI, you start by defining the provider and your credentials, then describe the resources you need. Ask Claude or Gemini in a Warp CLI session: "Write a Terraform configuration for an OCI project that provisions an Autonomous Database, a compute instance for my FastAPI backend, a VCN with security lists for HTTP, HTTPS, and SSH, IAM policies scoped to least privilege, and a Vault for storing secrets".&lt;/p&gt;

&lt;p&gt;The AI will generate a set of files. The structure typically looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;infra/
├── main.tf          # provider config and root module
├── variables.tf     # environment-specific values
├── outputs.tf       # values to export (e.g. DB connection string)
├── network.tf       # VCN, subnets, security lists
├── compute.tf       # your FastAPI server instance
├── database.tf      # Autonomous Database
├── iam.tf           # policies and dynamic groups
└── vault.tf         # OCI Vault and secrets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Before Terraform can talk to OCI, you'll need a few values that only exist in the console: your tenancy OCID, user OCID, region, and a generated API key fingerprint. These go into your variables.tf or a local terraform.tfvars file and are never committed to GitLab. If you're not sure where to find them, ask the AI: "Walk me through finding my OCI tenancy OCID and setting up an API signing key for Terraform". It will give you the exact navigation path through the console, and you can screenshot anything confusing and paste it directly into the chat.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A few things worth understanding in each file before you apply:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security lists&lt;/strong&gt; are OCI's firewall rules. They define which ports are open to the internet and which are locked down. Your FastAPI backend should only accept traffic on the ports it needs, typically 443 for HTTPS and a restricted port for database connections. Your database should not be publicly accessible at all. The Terraform configuration makes these rules explicit and auditable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;IAM policies&lt;/strong&gt; define what can access what within your OCI tenancy. The principle here is least privilege: your compute instance should only have the permissions it actually needs to do its job, nothing broader. When IAM is configured manually through the console it's easy to accidentally grant too much. When it's in a &lt;code&gt;.tf&lt;/code&gt; file it's readable, reviewable, and version-controlled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;OCI Vault&lt;/strong&gt; is where your secrets live: database passwords, API keys, third-party credentials. Your application reads them from Vault at runtime rather than having them hardcoded in environment files or committed to your repository. Ask the AI to generate both the Vault configuration in Terraform and the Python code in your FastAPI service that retrieves secrets from Vault on startup.&lt;/p&gt;

&lt;p&gt;Once your files are ready, initialise and apply from Warp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;infra
terraform init
terraform plan
terraform apply
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;terraform plan&lt;/code&gt; shows you exactly what will be created before anything happens. Read it. If something looks wrong, ask the AI to explain what a specific resource block does before you apply. Once you're satisfied, &lt;code&gt;apply&lt;/code&gt; provisions everything.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Newer versions of OCI Autonomous DB support TLS, or wallet-less, connections, which are significantly easier to configure with FastAPI than the older wallet zip file approach. Once your database is provisioned, ask the AI: "Help me enable TLS connections on my OCI Autonomous Database and configure FastAPI to connect without a wallet."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;From this point forward, any infrastructure change goes through a &lt;code&gt;.tf&lt;/code&gt; file, gets committed to GitLab, and gets applied via Terraform. You can add a Terraform stage to your GitLab CI/CD pipeline so infrastructure changes are applied automatically alongside code deployments, giving you a single pipeline that handles both application and infrastructure.&lt;/p&gt;

&lt;p&gt;If &lt;code&gt;apply&lt;/code&gt; throws errors, copy the output from Warp and paste it as a prompt. The AI will read the error, identify whether it's a credentials issue, a resource limit, a policy conflict, or a configuration mistake, and tell you exactly what to change.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/tomUWcQ0P3k?start=2"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 7: Push to GitLab and Set Up CI/CD
&lt;/h3&gt;

&lt;p&gt;Create a GitLab account at &lt;a href="https://gitlab.com/" rel="noopener noreferrer"&gt;gitlab.com&lt;/a&gt; and create a new project.&lt;/p&gt;

&lt;p&gt;This is one place where you stay in VS Code rather than switching to Warp. VS Code has a built-in Source Control panel, the branch icon in the left sidebar, and it handles the parts of Git that are most error-prone to do manually: staging individual files, writing commit messages, and reviewing exactly what's changed before it goes anywhere. Click the files you want to stage, write your commit message in the text field, and commit directly from there without touching the terminal.&lt;/p&gt;

&lt;p&gt;For the initial setup, you'll need the terminal once. Open VS Code's built-in terminal or use Warp for this part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git init
git remote add origin https://gitlab.com/yourusername/your-project.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that, your day-to-day flow is: make changes in VS Code, run &lt;code&gt;coderabbit review --plain --type uncommitted&lt;/code&gt; in Warp to review before committing, stage and commit through the VS Code Source Control sidebar, then push. You can push directly from the sidebar too using the sync button, or from Warp if you prefer the explicit &lt;code&gt;git push&lt;/code&gt;. Either works, but keeping the staging and commit message writing in VS Code means you always have a visual diff in front of you when you're deciding what to include in a commit.&lt;/p&gt;

&lt;p&gt;Now ask the AI to generate your &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; pipeline file. Give it context: "I have a FastAPI backend deployed on OCI and a Next.js frontend on Cloudflare Pages. Write a GitLab CI/CD pipeline that runs my tests on every push and deploys to production when I push to main."&lt;/p&gt;

&lt;p&gt;Once this is in place, your deployment process is automated. You make an edit, push it, and the pipeline handles the rest.&lt;/p&gt;

&lt;h4&gt;
  
  
  What goes in CI/CD variables vs. OCI Vault
&lt;/h4&gt;

&lt;p&gt;GitLab CI/CD has its own secrets store: the variables you set under Settings &amp;gt; CI/CD &amp;gt; Variables in your project. These are injected into the pipeline environment at runtime and are the right place for anything the pipeline itself needs to do its job: your OCI registry credentials so the build stage can push images, your SSH private key so the deploy stage can connect to the VM, and any tokens needed to authenticate with external services during the build.&lt;/p&gt;

&lt;p&gt;OCI Vault is for secrets your running application needs after it is deployed: database passwords, third-party API keys, encryption keys, and anything else your FastAPI backend reads at startup or during request handling. These never touch the pipeline directly.&lt;/p&gt;

&lt;p&gt;The distinction is about who needs the secret and when. If the pipeline needs it to build or deploy, it goes in GitLab CI/CD variables. If the running container needs it to serve requests, it goes in OCI Vault.&lt;/p&gt;

&lt;p&gt;In practice the line is usually clear, but edge cases come up. A good prompt for the AI is: "Here is a list of the secrets in my application. For each one, tell me whether it belongs in GitLab CI/CD variables or OCI Vault, and why." Give it your actual list and it will reason through each one, flag anything that looks like it might be in the wrong place, and explain the security rationale behind each decision. It is a quick sanity check that saves you from accidentally exposing something that should be locked away, or over-engineering the Vault integration for something that only the pipeline ever touches.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; GitLab CI/CD variables marked as "masked" are redacted from pipeline logs. Variables marked as "protected" are only available on protected branches. Use both for anything sensitive. The AI can generate the exact SSH key setup and OCI authentication configuration for your deploy stage if you ask it to.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;When a pipeline fails: open the failed job in GitLab, copy the log output, and paste it directly into Claude or Gemini as a prompt. Say: "This is my GitLab CI/CD pipeline log. It failed. What went wrong and how do I fix it?" Nine times out of ten, you'll have your answer in under a minute.&lt;/p&gt;

&lt;p&gt;That's the full loop. From idea to codebase to deployed infrastructure, with AI working alongside you at every stage and a pipeline handling everything after that.&lt;/p&gt;

&lt;h4&gt;
  
  
  A final word: logs are your best friend.
&lt;/h4&gt;

&lt;p&gt;When something breaks, and something always does, the logs tell you exactly where. Not approximately. Not probably. Exactly. This is one of the underrated benefits of building with a stack that has clear separation of concerns. When your backend, frontend, worker, proxy, and database each run in their own container with their own logs, a failure doesn't hide. It surfaces in one place, in one service, with a traceable reason. Copy those logs, paste them into Claude or Gemini, and you'll have a diagnosis in under a minute. The more deliberately you've structured your project, the more your logs reward you when things go sideways. Treat them as the source of truth, not an afterthought.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;Technology&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Terminal&lt;/td&gt;
&lt;td&gt;Warp&lt;/td&gt;
&lt;td&gt;Better AI CLI experience outside VS Code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud / DB&lt;/td&gt;
&lt;td&gt;Oracle Cloud (OCI) + Autonomous Database&lt;/td&gt;
&lt;td&gt;Mature, 10TB free egress, enterprise-grade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hosting / CDN&lt;/td&gt;
&lt;td&gt;Cloudflare Pages + Workers + Wrangler&lt;/td&gt;
&lt;td&gt;330+ city edge network, DDoS protection, no egress fees&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend&lt;/td&gt;
&lt;td&gt;FastAPI (Python)&lt;/td&gt;
&lt;td&gt;Fast, readable, excellent AI familiarity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Schema management&lt;/td&gt;
&lt;td&gt;Liquibase&lt;/td&gt;
&lt;td&gt;Versioned, rollback-capable, production-safe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Containerisation&lt;/td&gt;
&lt;td&gt;Docker + Docker Compose&lt;/td&gt;
&lt;td&gt;Isolated services, consistent environments, Kubernetes on-ramp&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Frontend&lt;/td&gt;
&lt;td&gt;Vanilla JS → React / Next.js&lt;/td&gt;
&lt;td&gt;Start simple, graduate when ready&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Mobile&lt;/td&gt;
&lt;td&gt;Flutter + Fastlane&lt;/td&gt;
&lt;td&gt;Cross-platform build, automated deployment&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Version control / CI/CD&lt;/td&gt;
&lt;td&gt;GitLab + .gitlab-ci.yml&lt;/td&gt;
&lt;td&gt;Integrated pipelines, paste logs to debug&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Infrastructure&lt;/td&gt;
&lt;td&gt;Terraform (from day one)&lt;/td&gt;
&lt;td&gt;Reproducible, codified infrastructure&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Code review&lt;/td&gt;
&lt;td&gt;CodeRabbit&lt;/td&gt;
&lt;td&gt;AI review before commit and on every MR&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The tools are remarkable. Use them. But use them on a foundation solid enough to build on, and use them properly, with a codebase the AI can actually see.&lt;/p&gt;




&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@jolin974658?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Jo Lin&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/laptop-with-ai-workspace-logo-on-screen-MSg17QHWf5Y?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>architecture</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Axios Hijack Post-Mortem: How to Audit, Pin, and Automate a Defense</title>
      <dc:creator>Smyekh David-West</dc:creator>
      <pubDate>Thu, 02 Apr 2026 01:09:47 +0000</pubDate>
      <link>https://dev.to/smyekh/axios-hijack-post-mortem-how-to-audit-pin-and-automate-a-defense-481d</link>
      <guid>https://dev.to/smyekh/axios-hijack-post-mortem-how-to-audit-pin-and-automate-a-defense-481d</guid>
      <description>&lt;p&gt;On March 31, 2026, the &lt;code&gt;axios&lt;/code&gt; npm package was compromised via a hijacked maintainer account. Two versions, &lt;code&gt;1.14.1&lt;/code&gt; and &lt;code&gt;0.30.4&lt;/code&gt;, were weaponised with a malicious phantom dependency called &lt;code&gt;plain-crypto-js&lt;/code&gt;. It functions as a Remote Access Trojan (RAT) that executes during the &lt;code&gt;postinstall&lt;/code&gt; phase and silently exfiltrates environment variables: AWS keys, GitHub tokens, database credentials, and anything present in your &lt;code&gt;.env&lt;/code&gt; at install time.&lt;/p&gt;

&lt;p&gt;The attack window was approximately 3 hours (00:21 to 03:29 UTC) before the packages were unpublished. A single CI run during that window is sufficient exposure.&lt;br&gt;
This post documents the forensic audit and remediation steps performed on a Next.js production stack immediately after the incident.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why This Happened: The SemVer Caret Problem
&lt;/h2&gt;

&lt;p&gt;Most projects define axios like this:&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="nl"&gt;"axios"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^1.7.9"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The caret (&lt;code&gt;^&lt;/code&gt;) permits any compatible minor or patch release. It means &lt;code&gt;npm install&lt;/code&gt; can silently resolve to a newly published &lt;code&gt;1.14.1&lt;/code&gt; if it satisfies the range. No prompt, no warning, no diff you would notice without inspecting the lockfile.&lt;/p&gt;

&lt;p&gt;This is the core tension in SemVer: convenience versus determinism.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Fix: Pin to the Golden Version
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"axios"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.14.0"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No caret. No tilde. Exact version.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;1.14.0&lt;/code&gt; specifically?&lt;/strong&gt; It is the last clean release before the March 31 hijack. It also includes the patch for &lt;a href="https://github.com/advisories/GHSA-jr5f-v2jv-69x6" rel="noopener noreferrer"&gt;CVE-2025-27152&lt;/a&gt;, an SSRF vulnerability fixed in &lt;code&gt;1.8.2&lt;/code&gt;, so you are not trading one vulnerability for another.&lt;/p&gt;

&lt;p&gt;Versions to avoid:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;1.14.1&lt;/code&gt; — compromised (RAT injected)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;0.30.4&lt;/code&gt; — compromised (RAT injected)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Use &lt;code&gt;--save-exact&lt;/code&gt; to prevent npm from re-adding the caret on install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;axios@1.14.0 &lt;span class="nt"&gt;--save-exact&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What If Axios Is a Transitive Dependency?
&lt;/h3&gt;

&lt;p&gt;If &lt;code&gt;axios&lt;/code&gt; is not a direct dependency in your project but a dependency of another package, pinning it in &lt;code&gt;package.json&lt;/code&gt; may not be sufficient. npm can still resolve a different version deeper in the tree.&lt;/p&gt;

&lt;p&gt;Use the &lt;code&gt;overrides&lt;/code&gt; field in &lt;code&gt;package.json&lt;/code&gt; to force the exact version project-wide, regardless of what upstream packages request:&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="nl"&gt;"overrides"&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;"axios"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1.14.0"&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;This applies to npm 8.3 and above. For yarn, the equivalent is &lt;code&gt;resolutions&lt;/code&gt;. For pnpm, use &lt;code&gt;overrides&lt;/code&gt; under the &lt;code&gt;pnpm&lt;/code&gt; key in &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Forensic Audit: Were You Hit?
&lt;/h2&gt;

&lt;p&gt;Even if &lt;code&gt;package.json&lt;/code&gt; looks correct, &lt;code&gt;package-lock.json&lt;/code&gt; may have resolved the malicious metadata during the attack window if &lt;code&gt;npm install&lt;/code&gt; ran between 00:21 and 03:29 UTC.&lt;/p&gt;

&lt;p&gt;The following checks were performed on the production server via SSH.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Lockfile Entropy Check
&lt;/h3&gt;

&lt;p&gt;Run both grep patterns. The first checks for the malicious dependency name and version in the axios context. The second checks for the raw version string regardless of JSON nesting depth, which is relevant in &lt;code&gt;package-lock.json&lt;/code&gt; v2 and v3 formats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Check 1: Malicious dependency name and axios version context&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"plain-crypto-js|axios.*(1&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;14&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;1|0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;30&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;4)"&lt;/span&gt; package-lock.json

&lt;span class="c"&gt;# Check 2: Raw version string, catches nested lockfile structures&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;version&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;(1&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;14&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;1|0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;30&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;4)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; package-lock.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Critical gotcha: run this from the correct directory.&lt;/code&gt; A &lt;code&gt;"file not found"&lt;/code&gt; error is not a clean result. It means grep never inspected anything. The correct workflow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;user@server:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"plain-crypto-js|axios.*(1&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;14&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;1|0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;30&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;4)"&lt;/span&gt; package-lock.json
&lt;span class="nb"&gt;grep&lt;/span&gt;: package-lock.json: No such file or directory   &lt;span class="c"&gt;# Wrong directory. Not a clean result.&lt;/span&gt;

user@server:~&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;project
user@server:~/project&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"plain-crypto-js|axios.*(1&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;14&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;1|0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;30&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;4)"&lt;/span&gt; package-lock.json
&lt;span class="nb"&gt;grep&lt;/span&gt;: package-lock.json: No such file or directory   &lt;span class="c"&gt;# Still wrong. Keep navigating.&lt;/span&gt;

user@server:~/project&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;frontend
user@server:~/project/frontend&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s2"&gt;"plain-crypto-js|axios.*(1&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;14&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;1|0&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;30&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="s2"&gt;4)"&lt;/span&gt; package-lock.json
&lt;span class="c"&gt;# No output = clean&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No output from the final command means no malicious indicators found in the lockfile.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Filesystem Dropper Verification
&lt;/h3&gt;

&lt;p&gt;The RAT executes via a &lt;code&gt;postinstall&lt;/code&gt; script registered by &lt;code&gt;plain-crypto-js&lt;/code&gt;. If the directory exists in &lt;code&gt;node_modules&lt;/code&gt;, the dropper has already run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;user@server:~/project/frontend&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;ls &lt;/span&gt;node_modules/plain-crypto-js 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"CRITICAL: Malicious package found!"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Clean: No dropper found."&lt;/span&gt;
Clean: No dropper found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;If either check returns a positive result:&lt;/strong&gt; rotate all secrets immediately. This includes credentials you might overlook under pressure:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Application secrets: database passwords, third-party API keys, cloud provider credentials&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CI/CD pipeline secrets: &lt;code&gt;NPM_TOKEN&lt;/code&gt;, &lt;code&gt;VERCEL_TOKEN&lt;/code&gt;, &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;, and any other tokens injected as environment variables during the build. These are present in the environment at npm install time and are a primary target of this class of attack.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treat every credential present in the build environment during the compromised install as leaked. Review your cloud provider audit logs for anomalous API calls originating from the build environment before doing anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating Prevention in CI/CD
&lt;/h2&gt;

&lt;p&gt;Pinning resolves the immediate exposure. The following three controls prevent recurrence.&lt;/p&gt;

&lt;h3&gt;
  
  
  A. Lockfile Integrity Enforcement
&lt;/h3&gt;

&lt;p&gt;Use &lt;code&gt;npm ci&lt;/code&gt; instead of &lt;code&gt;npm install&lt;/code&gt; in your build pipeline. It installs exactly what is recorded in package-lock.json and fails if the two files are out of sync. No silent resolution, no opportunistic upgrades.&lt;/p&gt;

&lt;p&gt;For Docker-based builds, this belongs in the &lt;code&gt;Dockerfile:&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  B. Dependency Scan as a Kill-Switch
&lt;/h3&gt;

&lt;p&gt;Add a pre-build security stage that greps the lockfile for known-malicious strings. If an upstream package is hijacked again, deployment is blocked before a single container is built.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitLab CI example&lt;/strong&gt; (shell executor, Docker available on runner):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;stages&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;security&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;deploy&lt;/span&gt;

&lt;span class="na"&gt;dependency_scan&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;security&lt;/span&gt;
  &lt;span class="na"&gt;tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;oci-runner&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
  &lt;span class="na"&gt;rules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;$CI_COMMIT_BRANCH&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;==&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;"main"'&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Scanning for malicious dependency versions..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
      &lt;span class="s"&gt;if grep -E "(\"axios\".*\"(1\.14\.1|0\.30\.4)\"|plain-crypto-js)" \&lt;/span&gt;
        &lt;span class="s"&gt;"$CI_PROJECT_DIR/frontend/package-lock.json"; then&lt;/span&gt;
          &lt;span class="s"&gt;echo "SECURITY ALERT: Malicious dependency detected. Blocking deployment."&lt;/span&gt;
          &lt;span class="s"&gt;exit 1&lt;/span&gt;
      &lt;span class="s"&gt;fi&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Malicious version scan passed."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;echo "Running npm audit..."&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;docker run --rm&lt;/span&gt;
        &lt;span class="s"&gt;-v "$CI_PROJECT_DIR/frontend:/app"&lt;/span&gt;
        &lt;span class="s"&gt;-w /app&lt;/span&gt;
        &lt;span class="s"&gt;node:20-alpine&lt;/span&gt;
        &lt;span class="s"&gt;npm audit --audit-level=high&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Implementation notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Do not use &lt;code&gt;-r&lt;/code&gt; in the grep.&lt;/strong&gt; That flag recurses into directories and is incorrect when targeting a specific file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;$CI_PROJECT_DIR&lt;/code&gt; for absolute paths.&lt;/strong&gt; Relative paths are fragile if any earlier step changes the working directory. This is the same "wrong directory" failure mode demonstrated in the manual audit above.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;&lt;code&gt;npm audit&lt;/code&gt; runs in a throwaway container.&lt;/strong&gt; If the shell runner does not have Node installed directly, a temporary &lt;code&gt;node:20-alpine&lt;/code&gt; container handles it without modifying the runner host.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;GitHub Actions equivalent:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Audit for Malicious Axios Versions&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;if grep -E "plain-crypto-js|axios.*(1\.14\.1|0\.30\.4)" package-lock.json; then&lt;/span&gt;
      &lt;span class="s"&gt;echo "Security Alert: Malicious dependency detected!"&lt;/span&gt;
      &lt;span class="s"&gt;exit 1&lt;/span&gt;
    &lt;span class="s"&gt;fi&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm audit&lt;/span&gt;
  &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm audit --audit-level=high&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  C. npm audit with Failure Thresholds
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;npm audit --audit-level=high&lt;/code&gt; causes CI to fail on any vulnerability rated High or Critical. It covers a broader class of supply chain issues beyond this specific incident and adds minimal overhead to the pipeline.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm audit &lt;span class="nt"&gt;--audit-level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;high
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One important caveat: for a zero-day or freshly published hijack, &lt;code&gt;npm audit&lt;/code&gt; may not flag the package until a security advisory has been formally ingested into the npm advisory database. During the initial hours of an active attack, the manual grep stage in section B is the more reliable immediate control. The two approaches are complementary, not interchangeable.&lt;/p&gt;

&lt;h3&gt;
  
  
  D. Egress Restriction on Build Runners
&lt;/h3&gt;

&lt;p&gt;As a systemic long-term control, restrict outbound network traffic from your build environment to a known allowlist of domains. A RAT cannot exfiltrate environment variables if the build server is blocked from making outbound requests to unknown IPs or domains.&lt;/p&gt;

&lt;p&gt;Most cloud providers offer security groups or firewall rules at the instance level. For OCI, this is configured via the VCN's security list or network security groups. The build runner should be permitted to reach package registries, your container registry, and your deployment target and nothing else by default.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One layer deeper:&lt;/strong&gt; blocking standard HTTP/S egress is not sufficient against advanced RATs. DNS exfiltration is a documented technique where data is encoded and tunnelled out via DNS queries, which most firewall rules pass freely. If your threat model warrants it, implement DNS filtering and logging on build runners either via a resolver that blocks non-allowlisted domains, or a logging layer that surfaces anomalous query volumes. This is the logical next control once HTTP/S egress is locked down.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Worth knowing:&lt;/strong&gt; If supply chain risk is a recurring concern for your stack, look into &lt;a href="https://socket.dev/" rel="noopener noreferrer"&gt;Socket&lt;/a&gt; or &lt;a href="https://snyk.io/" rel="noopener noreferrer"&gt;Snyk&lt;/a&gt;. Both offer malicious package detection that goes beyond standard vulnerability scanning by analysing package behaviour rather than just matching against known CVEs. npm audit tells you about published advisories. These tools flag suspicious patterns before an advisory exists. Both have free tiers suitable for open source projects and solo developers; private commercial repositories require a paid plan.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Priority&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pin axios to &lt;code&gt;1.14.0&lt;/code&gt;(no caret)&lt;/td&gt;
&lt;td&gt;Immediate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add &lt;code&gt;overrides&lt;/code&gt; in &lt;code&gt;package.json&lt;/code&gt; if axios is a transitive dependency&lt;/td&gt;
&lt;td&gt;Immediate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grep lockfile for &lt;code&gt;plain-crypto-js&lt;/code&gt; and bad versions, from the correct directory&lt;/td&gt;
&lt;td&gt;Immediate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Check whether &lt;code&gt;node_modules/plain-crypto-js&lt;/code&gt; exists&lt;/td&gt;
&lt;td&gt;Immediate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rotate all secrets if either check is positive&lt;/td&gt;
&lt;td&gt;Immediate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Switch CI builds to &lt;code&gt;npm ci&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;This sprint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add dependency scan stage to pipeline&lt;/td&gt;
&lt;td&gt;This sprint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Add &lt;code&gt;npm audit --audit-level=high&lt;/code&gt; to CI&lt;/td&gt;
&lt;td&gt;This sprint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restrict build runner egress to known domains&lt;/td&gt;
&lt;td&gt;Next sprint&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Implement DNS filtering and logging on build runners&lt;/td&gt;
&lt;td&gt;Next sprint&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Supply chain attacks exploit the trust relationship between developers and the package registry. The &lt;code&gt;plain-crypto-js&lt;/code&gt; incident demonstrates that a single compromised maintainer account is sufficient to poison any project that does not lock its dependencies with exact versions.&lt;/p&gt;

&lt;p&gt;Pin your versions. Audit your lockfiles. If your build logs show &lt;code&gt;npm install&lt;/code&gt; activity on March 31, rotate credentials first and investigate second.&lt;/p&gt;




&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@cbpsc1?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Clint Patterson&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/man-siting-facing-laptop-dYEuFB8KQJk?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>devops</category>
      <category>node</category>
    </item>
    <item>
      <title>Rebooting a Production VM on Oracle Cloud: A Reference Guide</title>
      <dc:creator>Smyekh David-West</dc:creator>
      <pubDate>Mon, 30 Mar 2026 17:19:18 +0000</pubDate>
      <link>https://dev.to/smyekh/rebooting-a-production-vm-on-oracle-cloud-a-reference-guide-3l8</link>
      <guid>https://dev.to/smyekh/rebooting-a-production-vm-on-oracle-cloud-a-reference-guide-3l8</guid>
      <description>&lt;p&gt;&lt;em&gt;Commands, explanations, and real output — for engineers who want to understand what's actually happening, not just copy-paste their way through it.&lt;/em&gt;&lt;/p&gt;




&lt;h3&gt;
  
  
  ☁️ Pre-Flight Checklist
&lt;/h3&gt;

&lt;p&gt;Before we taxi down the runway, here’s your flight plan. Keep this handy to navigate your flight path. &lt;strong&gt;Welcome aboard the cloud!&lt;/strong&gt; ☁️&lt;/p&gt;

&lt;h3&gt;
  
  
  🌥️ Takeoff
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⛅️ Cruising Altitude
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Part 1 — Pre-Reboot Checklist&lt;/li&gt;
&lt;li&gt;Part 2 — Running the Reboot&lt;/li&gt;
&lt;li&gt;Part 3 — Post-Reboot Verification&lt;/li&gt;
&lt;li&gt;Part 4 — Measuring Time to Recovery (TTR)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌤️ Landing &amp;amp; Taxi
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Quick Reference: All Commands&lt;/li&gt;
&lt;li&gt;Troubleshooting Reference&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Enjoy your flight! ☁️&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;There's a specific kind of anxiety that comes with running &lt;code&gt;sudo reboot&lt;/code&gt; on a server with real users on it. You know the system should come back, but "should" feels a lot less reassuring at the moment your SSH session freezes. This guide removes the guesswork. It covers everything from reading your &lt;code&gt;apt upgrade&lt;/code&gt; output intelligently, to verifying your stack is healthy after the reboot, to measuring your actual recovery time with real commands and real numbers so that the next time you need to do this, it's a procedure, not a gamble.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;This guide assumes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ubuntu 22.04 on an OCI Compute instance (ARM or x86)&lt;/li&gt;
&lt;li&gt;Docker + Docker Compose managing your services&lt;/li&gt;
&lt;li&gt;All long-running services configured with &lt;code&gt;restart: always&lt;/code&gt; in your &lt;code&gt;docker-compose.yml&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;SSH access to the instance&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If &lt;code&gt;restart: always&lt;/code&gt; isn't set on your services, your containers will &lt;strong&gt;not&lt;/strong&gt; come back after a reboot. Check this first.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-backend-image&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;  &lt;span class="c1"&gt;# ✅ restarts automatically after reboot or crash&lt;/span&gt;

  &lt;span class="na"&gt;migrations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-migrations-image&lt;/span&gt;
    &lt;span class="c1"&gt;# no restart policy  # ✅ correct — this should run once and exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;restart: always&lt;/code&gt; tells Docker to relaunch the container whenever it stops — whether from a crash or a full system reboot. The one exception to be deliberate about is one-shot containers like database migrations: they're designed to run once and exit cleanly, so no restart policy is the right call for those.&lt;/p&gt;

&lt;h2&gt;
  
  
  Part 1 — Pre-Reboot Checklist
&lt;/h2&gt;

&lt;p&gt;Never reboot without completing this checklist. It takes under two minutes and prevents the most common post-reboot problems.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.1 Verify no critical process is mid-flight
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to look for:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;STATUS&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Up 2 days (healthy)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Safe to reboot&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Up 3 minutes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Something recently restarted — investigate&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Restarting (1)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Container is crash-looping — fix before rebooting&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Up 2 hours (unhealthy)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Health check is failing — fix before rebooting&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If everything shows &lt;code&gt;Up [days/weeks] (healthy)&lt;/code&gt;, you are clear.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; If a database migration container is mid-run, or a background job is processing a large task, a reboot will kill it mid-execution. You want to reboot during a quiet moment.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.2 Validate your Compose configuration
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/your-project
docker compose config
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected output:&lt;/strong&gt; Your full resolved &lt;code&gt;docker-compose.yml&lt;/code&gt; printed to the terminal, with no errors.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this matters:&lt;/strong&gt; &lt;code&gt;docker compose config&lt;/code&gt; resolves all environment variables and validates YAML syntax. If there's a broken variable reference or a typo in your file, this command catches it &lt;em&gt;now&lt;/em&gt; — not after the reboot when containers silently fail to start. A common mistake is editing a &lt;code&gt;.env&lt;/code&gt; file or &lt;code&gt;docker-compose.yml&lt;/code&gt; and not realising you've introduced a syntax error. This is your safety net.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.3 Read your &lt;code&gt;apt upgrade&lt;/code&gt; output
&lt;/h3&gt;

&lt;p&gt;When you run &lt;code&gt;sudo apt update &amp;amp;&amp;amp; sudo apt upgrade -y&lt;/code&gt; before a reboot, the output tells you exactly what changed on your system. Don't skip past it.&lt;/p&gt;

&lt;p&gt;Here's a real upgrade output and what each part means:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The following packages will be upgraded:
  containerd.io coreutils docker-ce docker-ce-cli
  docker-ce-rootless-extras docker-compose-plugin docker-model-plugin
  gitlab-runner gitlab-runner-helper-images libnftables1 nftables
  python3-pyasn1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;How to read this list:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Package&lt;/th&gt;
&lt;th&gt;What it is&lt;/th&gt;
&lt;th&gt;Reboot needed?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;docker-ce&lt;/code&gt;, &lt;code&gt;containerd.io&lt;/code&gt;, &lt;code&gt;docker-ce-cli&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;The Docker engine and its runtime&lt;/td&gt;
&lt;td&gt;Recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker-compose-plugin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The &lt;code&gt;docker compose&lt;/code&gt; CLI plugin&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;nftables&lt;/code&gt;, &lt;code&gt;libnftables1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Linux kernel firewall/networking&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;coreutils&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fundamental Linux utilities (&lt;code&gt;ls&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, etc.)&lt;/td&gt;
&lt;td&gt;Recommended&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;gitlab-runner&lt;/code&gt;, &lt;code&gt;gitlab-runner-helper-images&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;CI/CD runner agent&lt;/td&gt;
&lt;td&gt;Service restarts during upgrade&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;python3-pyasn1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Python crypto library&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;The rule of thumb:&lt;/strong&gt; If the upgrade touches anything in the kernel, networking stack, or container runtime — reboot. If it's only application-level packages — a reboot is optional but never harmful.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.4 Understand the service restart messages
&lt;/h3&gt;

&lt;p&gt;After &lt;code&gt;apt upgrade&lt;/code&gt;, Ubuntu's &lt;code&gt;needrestart&lt;/code&gt; tool prints which services were restarted automatically and which were deferred:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Restarting services...
 systemctl restart irqbalance.service ssh.service rsyslog.service ...

Service restarts being deferred:
 systemctl restart networkd-dispatcher.service
 systemctl restart systemd-logind.service
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;"Restarting services"&lt;/strong&gt; — These were restarted immediately. Your SSH connection stayed alive because &lt;code&gt;ssh.service&lt;/code&gt; restarts in-place without dropping existing sessions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Service restarts being deferred"&lt;/strong&gt; — These require a full reboot to apply safely. &lt;code&gt;systemd-logind&lt;/code&gt; manages user sessions; restarting it mid-session can cause issues, so Ubuntu defers it to the next clean boot.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No containers need to be restarted.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This line means Docker detected that running container images are still current — no container needed to be replaced. This is expected if you haven't rebuilt your application images.&lt;/p&gt;

&lt;h3&gt;
  
  
  1.5 Check available disk space
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; /
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Filesystem      Size  Used Avail Use% Mounted on
/dev/sda1        48G   12G   36G  23% /
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You want at least 20% free on your root partition. Docker image pulls and accumulated log files are the two most common causes of a full disk, which can prevent containers from starting after a reboot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; The &lt;code&gt;apt upgrade&lt;/code&gt; process often reclaims space automatically by pruning unused Docker build cache layers. In a real upgrade run, this printed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total reclaimed space: 4.165GB
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Part 2 — Running the Reboot
&lt;/h2&gt;

&lt;p&gt;Once the checklist is complete:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What happens next, step by step:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The OS sends &lt;code&gt;SIGTERM&lt;/code&gt; to all running processes, giving them time to shut down cleanly.&lt;/li&gt;
&lt;li&gt;Docker receives the signal and stops all containers gracefully.&lt;/li&gt;
&lt;li&gt;The kernel shuts down and the VM restarts.&lt;/li&gt;
&lt;li&gt;Your SSH session prints &lt;code&gt;Connection to [ip] closed by remote host.&lt;/code&gt; and terminates. This is normal.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;How long to wait:&lt;/strong&gt; OCI ARM instances (Ampere A1) typically reboot in 45–90 seconds. Wait at least 60 seconds before trying to reconnect.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/id_rsa ubuntu@YOUR_IP
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Part 3 — Post-Reboot Verification
&lt;/h2&gt;

&lt;p&gt;Run these checks in order. Each one builds on the last.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.1 Check the Docker daemon
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status docker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight systemd"&gt;&lt;code&gt;&lt;span class="err"&gt;●&lt;/span&gt; &lt;span class="err"&gt;docker.service&lt;/span&gt; &lt;span class="err"&gt;-&lt;/span&gt; &lt;span class="err"&gt;Docker&lt;/span&gt; &lt;span class="err"&gt;Application&lt;/span&gt; &lt;span class="err"&gt;Container&lt;/span&gt; &lt;span class="err"&gt;Engine&lt;/span&gt;
     &lt;span class="err"&gt;Loaded:&lt;/span&gt; &lt;span class="err"&gt;loaded&lt;/span&gt; &lt;span class="err"&gt;(/lib/systemd/system/docker.service&lt;/span&gt;&lt;span class="c"&gt;; enabled)&lt;/span&gt;
     &lt;span class="err"&gt;Active:&lt;/span&gt; &lt;span class="err"&gt;active&lt;/span&gt; &lt;span class="err"&gt;(running)&lt;/span&gt; &lt;span class="err"&gt;since&lt;/span&gt; &lt;span class="err"&gt;Mon&lt;/span&gt; &lt;span class="err"&gt;2026-03-30&lt;/span&gt; &lt;span class="err"&gt;15:55:51&lt;/span&gt; &lt;span class="err"&gt;UTC&lt;/span&gt;&lt;span class="c"&gt;; 5min ago&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key things to check:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Active: active (running)&lt;/code&gt; — the daemon is running ✅&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;enabled&lt;/code&gt; — it is configured to auto-start on every future boot ✅&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If the daemon isn't running:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;docker   &lt;span class="c"&gt;# ensure it starts on future reboots&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start docker    &lt;span class="c"&gt;# start it now&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3.2 Check all containers are up
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;CONTAINER ID   IMAGE              COMMAND        CREATED      STATUS                  PORTS    NAMES
fc46f84c7bd5   app-backend        "uv run uvi…"  2 days ago   Up 5 minutes (healthy)  8000/tcp app_backend
a3e9a2eeb160   redis:alpine       "docker-ent…"  2 weeks ago  Up 5 minutes (healthy)  6379/tcp app_redis
f4afe2edb00c   caddy:alpine       "caddy run …"  4 weeks ago  Up 5 minutes (healthy)  80, 443  caddy_proxy
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What to check:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Every service you expect should be present. If one is missing, it crashed on startup.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;STATUS&lt;/code&gt; should be &lt;code&gt;Up&lt;/code&gt; or &lt;code&gt;Up (healthy)&lt;/code&gt;. &lt;code&gt;(health: starting)&lt;/code&gt; is fine for the first 30 seconds after boot.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;CREATED&lt;/code&gt; timestamp does &lt;strong&gt;not&lt;/strong&gt; reset on reboot — it reflects when the container was first created with &lt;code&gt;docker compose up&lt;/code&gt;. This is normal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;If a container is missing or in a restart loop:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose logs &lt;span class="o"&gt;[&lt;/span&gt;service_name] &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This shows the last 50 log lines for that specific service, which will usually tell you exactly why it failed.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.3 Watch the live logs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd&lt;/span&gt; ~/your-project
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-f&lt;/code&gt; flag follows the log stream in real time. &lt;code&gt;--tail=20&lt;/code&gt; shows the last 20 lines per service as a starting point.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What healthy output looks like:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;app_gate    | 127.0.0.1 - - [30/Mar/2026:16:00:00 +0000] "GET / HTTP/1.1" 200 4140
app_backend | INFO: 127.0.0.1:58562 - "GET /health HTTP/1.1" 200 OK
caddy_proxy | {"level":"info","msg":"received request","uri":"/config/"}
app_redis   | * Ready to accept connections tcp
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;What a transient (non-critical) error looks like:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;app_worker | redis.exceptions.ConnectionError: Error while reading
            from redis:6379 : (104, 'Connection reset by peer')
app_worker | 15:56:15: Starting worker for 1 functions: process_message
app_worker | 15:56:15: redis_version=8.6.1 mem_usage=1.38M clients_connected=1
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern — an error followed immediately by a successful connection message — is &lt;strong&gt;normal during cold starts&lt;/strong&gt;. When all containers launch simultaneously, a dependent service (like a worker) may attempt its first connection before its dependency (like Redis) has finished initialising. The container retries and connects successfully on the next attempt. This is expected behavior.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What a critical error looks like:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;app_backend | sqlalchemy.exc.OperationalError: connection refused
app_backend | [after 5 retries] giving up
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A critical error is one that does &lt;em&gt;not&lt;/em&gt; resolve on its own. If you see continuous errors without a recovery line following them, press &lt;code&gt;Ctrl+C&lt;/code&gt; and investigate that service.&lt;/p&gt;

&lt;h3&gt;
  
  
  3.4 Check additional system services
&lt;/h3&gt;

&lt;p&gt;If you run a CI/CD runner or similar agent alongside Docker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;gitlab-runner status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Expected output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;gitlab-runner: Service is running
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If it's not running:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;gitlab-runner start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Part 4 — Measuring Time to Recovery (TTR)
&lt;/h2&gt;

&lt;p&gt;TTR is the total time from &lt;code&gt;sudo reboot&lt;/code&gt; to the moment your application is serving healthy responses. Measuring it gives you accurate data for maintenance window planning and user communications.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.1 Measure OS boot time
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Startup finished in 3.617s (kernel) + 19.608s (userspace) = 23.225s
graphical.target reached after 18.845s in userspace
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Breaking this down:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Phase&lt;/th&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;What's happening&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Kernel&lt;/td&gt;
&lt;td&gt;3.6s&lt;/td&gt;
&lt;td&gt;The Linux kernel loads into memory and initialises hardware drivers&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Userspace&lt;/td&gt;
&lt;td&gt;19.6s&lt;/td&gt;
&lt;td&gt;All systemd services start in parallel (networking, Docker, SSH, etc.)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;23.2s&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OS is fully booted&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4.2 Find the bottleneck in the boot sequence
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;systemd-analyze blame | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lists every service sorted by how long it took to start, slowest first:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;12.186s docker.service
 4.821s cloud-init.service
 1.204s snapd.service
   38ms docker.socket
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In this case, Docker itself accounted for 12 of the 23 total seconds. This is normal — Docker has to read its state from disk, re-attach networks, and prepare to launch containers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why this is useful:&lt;/strong&gt; If your boot time is unexpectedly long, &lt;code&gt;systemd-analyze blame&lt;/code&gt; tells you exactly which service is the bottleneck.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.3 Find the exact moment containers started
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{{.Name}}: {{.State.StartedAt}}'&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;docker ps &lt;span class="nt"&gt;-q&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example output:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/app_ftp_bridge:  2026-03-30T15:55:57.766Z
/app_worker:      2026-03-30T15:55:57.695Z
/app_backend:     2026-03-30T15:55:57.646Z
/app_gate:        2026-03-30T15:55:57.830Z
/app_admin:       2026-03-30T15:55:57.742Z
/app_redis:       2026-03-30T15:55:57.794Z
/caddy_proxy:     2026-03-30T15:55:57.615Z
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every container launched within the same second. This is because Docker starts all containers in parallel as soon as the daemon is ready. Note: this timestamp reflects when Docker &lt;em&gt;launched&lt;/em&gt; the container process, not when the application inside it was ready to serve traffic. A container may take a further 5–30 seconds to pass its health check after this point.&lt;/p&gt;

&lt;h3&gt;
  
  
  4.4 Build your full TTR timeline
&lt;/h3&gt;

&lt;p&gt;Combining the data from the above commands:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Time (relative to reboot)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;sudo reboot&lt;/code&gt; executed&lt;/td&gt;
&lt;td&gt;T+0s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH connection closed&lt;/td&gt;
&lt;td&gt;T+~5s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Kernel boot complete&lt;/td&gt;
&lt;td&gt;T+~8s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Userspace boot complete (OS ready)&lt;/td&gt;
&lt;td&gt;T+~28s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker daemon ready&lt;/td&gt;
&lt;td&gt;T+~28s (12s of the userspace phase)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All containers launched&lt;/td&gt;
&lt;td&gt;T+~28s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redis accepting connections&lt;/td&gt;
&lt;td&gt;T+~30s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Backend &lt;code&gt;/health&lt;/code&gt; returning 200&lt;/td&gt;
&lt;td&gt;T+~35s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All health checks passing&lt;/td&gt;
&lt;td&gt;T+~55s&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total TTR&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~55–60 seconds&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  4.5 Use TTR to plan user communications
&lt;/h3&gt;

&lt;p&gt;With a measured TTR, you can set honest expectations.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Internal / engineering team:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Maintenance reboot at [time]. Expected downtime: ~2 minutes."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The 2-minute internal window gives a buffer above the measured ~60 seconds for anything unexpected.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;External users:&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Scheduled maintenance in progress. Services will be restored within 5 minutes."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The 5-minute external window is deliberately conservative. If a container fails its first health check and requires a full restart cycle (up to 5 retries × 5 seconds = 25 extra seconds), you're still within your stated window. Under-promise, over-deliver.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Reference: All Commands
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# --- PRE-REBOOT ---&lt;/span&gt;
docker ps                              &lt;span class="c"&gt;# check container states&lt;/span&gt;
docker compose config                  &lt;span class="c"&gt;# validate compose file syntax&lt;/span&gt;
&lt;span class="nb"&gt;df&lt;/span&gt; &lt;span class="nt"&gt;-h&lt;/span&gt; /                                &lt;span class="c"&gt;# check available disk space&lt;/span&gt;

&lt;span class="c"&gt;# --- REBOOT ---&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;reboot                            &lt;span class="c"&gt;# initiate the reboot&lt;/span&gt;

&lt;span class="c"&gt;# --- POST-REBOOT ---&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl status docker           &lt;span class="c"&gt;# confirm daemon is running&lt;/span&gt;
docker ps                              &lt;span class="c"&gt;# confirm containers are up&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="nt"&gt;--tail&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;20       &lt;span class="c"&gt;# watch live logs&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;gitlab-runner status              &lt;span class="c"&gt;# check runner (if applicable)&lt;/span&gt;

&lt;span class="c"&gt;# --- TTR MEASUREMENT ---&lt;/span&gt;
systemd-analyze                        &lt;span class="c"&gt;# total OS boot time&lt;/span&gt;
systemd-analyze blame | &lt;span class="nb"&gt;head&lt;/span&gt; &lt;span class="nt"&gt;-20&lt;/span&gt;       &lt;span class="c"&gt;# per-service boot time breakdown&lt;/span&gt;
docker inspect &lt;span class="nt"&gt;--format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'{{.Name}}: {{.State.StartedAt}}'&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;docker ps &lt;span class="nt"&gt;-q&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
                                       &lt;span class="c"&gt;# exact container start timestamps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Troubleshooting Reference
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Likely cause&lt;/th&gt;
&lt;th&gt;Fix&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Container missing from &lt;code&gt;docker ps&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Crashed on startup&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose logs [service] --tail=50&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Container stuck in &lt;code&gt;(health: starting)&lt;/code&gt; after 2+ minutes&lt;/td&gt;
&lt;td&gt;Health check command failing&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;docker inspect [id]&lt;/code&gt; → check &lt;code&gt;Health.Log&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Docker daemon not running&lt;/td&gt;
&lt;td&gt;Not enabled in systemd&lt;/td&gt;
&lt;td&gt;&lt;code&gt;sudo systemctl enable docker &amp;amp;&amp;amp; sudo systemctl start docker&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SSH times out for more than 3 minutes&lt;/td&gt;
&lt;td&gt;VM didn't boot cleanly&lt;/td&gt;
&lt;td&gt;Check OCI console → instance serial console for kernel panic output&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;All containers up but app unreachable externally&lt;/td&gt;
&lt;td&gt;Reverse proxy (Caddy/Nginx) issue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;docker compose logs caddy --tail=50&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Persistent container errors after cold start&lt;/td&gt;
&lt;td&gt;Dependency started before its dependency was ready&lt;/td&gt;
&lt;td&gt;Wait 60 seconds, then re-check — most resolve automatically&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@boliviainteligente?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;BoliviaInteligente&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/text-_BFJOg1nxaw?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>docker</category>
      <category>linux</category>
      <category>cloud</category>
    </item>
    <item>
      <title>Why the Best Errors Are ORA Errors: A Love Letter to Loud Failures</title>
      <dc:creator>Smyekh David-West</dc:creator>
      <pubDate>Mon, 29 Dec 2025 05:57:53 +0000</pubDate>
      <link>https://dev.to/smyekh/why-the-best-errors-are-ora-errors-a-love-letter-to-loud-failures-6gk</link>
      <guid>https://dev.to/smyekh/why-the-best-errors-are-ora-errors-a-love-letter-to-loud-failures-6gk</guid>
      <description>&lt;p&gt;Picture this: you're deploying to production on a Friday afternoon. Your code runs smoothly in development, passes all your tests, and then quietly corrupts user data in prod. No error messages. No warnings. Just silent data corruption that takes weeks to discover and months to untangle.&lt;/p&gt;

&lt;p&gt;Now picture the alternative: Oracle Database throws an ORA-00001 unique constraint violation the moment you try to insert duplicate data in development. The error is immediate and precise and tells you exactly which constraint you violated. You fix it in five minutes and move on with your life.&lt;/p&gt;

&lt;p&gt;Which scenario would you prefer? If you chose the first one, we need to talk about why loud, explicit errors are actually your best friends as a developer.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Oracle Philosophy: Fail Fast, Fail Clearly
&lt;/h2&gt;

&lt;p&gt;Oracle Database has been around since 1979, and in that time it's developed a reputation for being strict, unforgiving, and sometimes downright pedantic. This isn't a bug; it's the entire point. Enterprise databases were built with a core principle: it's better to reject questionable operations loudly than to accept them silently and create data integrity problems down the line.&lt;/p&gt;

&lt;p&gt;Consider the classic ORA-00904 error for invalid identifiers. When you write a query that references a column like uid, Oracle might throw this error and force you to use p_uid instead. At first glance, this seems annoying. Why can't the database just figure out what you meant? But here's what's actually happening: Oracle is protecting you from ambiguity. In a complex system with multiple schemas, packages, and procedures, a column reference that could mean two different things is a disaster waiting to happen. By forcing explicit qualification, Oracle ensures that six months from now, when someone else is reading your code (or when you've forgotten your own clever shortcuts), the intent is crystal clear.&lt;/p&gt;

&lt;p&gt;This same protective instinct appears in Oracle's handling of bind variables. Consider the ORA-01745 error that occurs when you try to use &lt;code&gt;:uid&lt;/code&gt; as a bind variable name. Oracle rejects this with "invalid host/bind variable name" and forces you to use something like &lt;code&gt;:p_uid&lt;/code&gt; instead. On the surface, this feels unnecessarily restrictive. You might wonder why Oracle cares what you name your variables, especially since uid seems like a perfectly reasonable name for a user identifier. But here's what's actually happening: uid is a reserved keyword in Oracle SQL. By rejecting &lt;code&gt;:uid&lt;/code&gt; as a bind variable name, Oracle protects you from creating queries where it's ambiguous whether you're referring to the reserved keyword or your bind variable. Six months from now, when you or another developer reads this query, there's no confusion about what &lt;code&gt;:p_uid&lt;/code&gt; refers to. The naming convention (using a prefix like &lt;code&gt;p_&lt;/code&gt; for parameters) becomes self-documenting. What seems like pedantry is actually preventing ambiguity, protecting against potential SQL injection vulnerabilities, and ensuring the database can properly parse and cache your statements. Oracle doesn't just protect you from ambiguous column references; it also protects you from unsafe query construction practices that could compromise your entire application.&lt;/p&gt;

&lt;p&gt;The ORA error codes themselves are a feature worth appreciating. When you see ORA-01400, you immediately know you've got a NOT NULL constraint violation. ORA-02291 means a foreign key constraint failed. These aren't vague "something went wrong" messages; they're diagnostic breadcrumbs that lead you directly to the problem. Every Oracle developer learns to recognize these codes, and that shared vocabulary makes debugging and collaboration significantly easier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Modern Web Frameworks Are Learning the Same Lesson
&lt;/h2&gt;

&lt;p&gt;FastAPI and Pydantic have brought this same philosophy to the Python web development world, and the results are striking. When you define a Pydantic schema for your API endpoint, you're not just documenting what the payload should look like. You're creating an enforcement mechanism that protects your application from malformed data.&lt;/p&gt;

&lt;p&gt;Here's what makes this powerful: if a field isn't explicitly defined in your Pydantic model, it won't appear in the validated payload. No sneaky extra fields slipping through. No undocumented parameters causing weird side effects. If a client sends data your schema doesn't expect, Pydantic catches it immediately and returns a detailed validation error. The alternative approach, accepting any JSON that gets sent and hoping your application code handles it correctly, is how you end up with production bugs that are nearly impossible to trace.&lt;/p&gt;

&lt;p&gt;The beauty of this strict validation is that it doesn't just catch mistakes; it documents intent. When another developer looks at your Pydantic models, they see exactly what your API expects. When they try to send the wrong data type, they get immediate feedback about what went wrong. This is the same principle Oracle has been practicing for decades: make the implicit explicit, and fail loudly when expectations aren't met.&lt;/p&gt;

&lt;p&gt;The difference between Pydantic's approach and truly silent failures is worth examining. While Pydantic doesn't always throw loud errors for every possible issue (for instance, extra fields might be silently ignored depending on your configuration), it gives you the tools to make validation as strict as you need. You can configure it to forbid extra fields, require specific data types, and validate complex business rules. The point is that you're given the mechanism to fail explicitly rather than letting problems propagate silently through your system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Liquibase and the Error Chain
&lt;/h2&gt;

&lt;p&gt;Liquibase provides an interesting case study in how tools can amplify or suppress error signals from underlying systems. When you're using Liquibase with Oracle Database, you get the best of both worlds: Liquibase's change tracking and rollback capabilities combined with Oracle's precise error reporting.&lt;/p&gt;

&lt;p&gt;If a migration fails because it violates a database constraint, Liquibase doesn't swallow that error or translate it into something generic. Instead, it passes through the ORA error code and message, giving you the full context you need to understand what went wrong. This transparency is crucial. It means you can leverage Oracle's diagnostic capabilities even when working through an abstraction layer.&lt;/p&gt;

&lt;p&gt;This behavior contrasts sharply with tools that try to be too helpful by wrapping errors in their own error-handling logic. When an ORM or migration tool catches a database error and then throws a generic "migration failed" exception, you've lost valuable information. The original error code might tell you exactly which constraint was violated or which column had a type mismatch, but now you're left guessing. Liquibase gets this right by staying out of the way and letting the database speak for itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Silent Killers: When Errors Don't Happen
&lt;/h2&gt;

&lt;p&gt;The most dangerous errors aren't the loud ones that stop your code from running. They're the silent ones that let bad data or an invalid state slip into your system. These are the issues that compound over time, creating data integrity problems that are extraordinarily difficult to unwind.&lt;/p&gt;

&lt;p&gt;Consider what happens when a system accepts invalid email addresses because there's no validation at the input layer. The data gets stored and processed, and eventually someone tries to send an email to "user@@example..com", and it fails. Now you have to trace back through potentially months of data to figure out where the invalid address came from, whether it represents a real user, and how to fix it without losing legitimate information. If you had simply validated the email format strictly at the input boundary and thrown an error for malformed addresses, this problem never would have existed.&lt;/p&gt;

&lt;p&gt;Or imagine an application that allows NULL values in a database column that represents a critical business relationship, like the customer ID on an order. Without a NOT NULL constraint, someone's buggy code can create orders that aren't attached to any customer. These orphaned records break reports, corrupt analytics, and create customer service nightmares when users can't see their orders. A simple constraint would have prevented the bug from ever making it to production by failing immediately when the invalid data was attempted.&lt;/p&gt;

&lt;p&gt;The pattern here is clear: the cost of fixing a bug grows exponentially with how long it takes to discover. Errors that fire immediately when invalid data is introduced are cheap to fix. Errors that only manifest after the data has propagated through multiple systems, been aggregated in reports, and influenced business decisions are catastrophically expensive to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exception Handling: Anticipating vs. Discovering Problems
&lt;/h2&gt;

&lt;p&gt;Understanding how to handle errors properly requires recognizing a crucial distinction between two fundamentally different categories of errors. There are errors we can anticipate and handle gracefully, and there are errors that indicate fundamental problems with our code. Think of these as recoverable errors versus programming errors.&lt;/p&gt;

&lt;p&gt;Recoverable errors are situations where the system is working exactly as designed, but the specific operation can't complete. A perfect example is ORA-00001, the unique constraint violation. When a user tries to register with an email address that's already in the database, Oracle throws this error. Your application should catch this specific error code, recognize what happened, and show the user a helpful message like "This email is already registered. Did you mean to log in instead?" You anticipated this possibility, you built handling for it, and the error becomes part of your normal application flow. This is good exception handling because it transforms a technical database error into a user-friendly experience.&lt;/p&gt;

&lt;p&gt;Programming errors tell a different story entirely. These are ORA errors that reveal bugs in your code. Consider ORA-01722, the invalid number error that occurs when you're trying to insert text into a numeric column. This shouldn't be caught and handled gracefully because it indicates your code is fundamentally broken. You've somehow allowed non-numeric data to reach a point where it's being inserted into a numeric field. This error should bubble up loudly in development, crash your tests, and force you to fix the root cause rather than papering over it with exception handling. Catching and suppressing this error would be dangerous because it masks a deeper problem in your data flow or validation logic.&lt;/p&gt;

&lt;p&gt;The powerful insight here is that Oracle's explicit, numbered error codes let you make this distinction clearly in your code. When you write your exception handling, you can catch the specific ORA codes you expect (like ORA-00001 for duplicate keys or ORA-02291 for foreign key violations) and let everything else fail. This is far better than databases or frameworks that give you one generic "database error" exception, forcing you to either catch everything (thereby hiding real bugs) or catch nothing (thereby making legitimate error cases crash your application).&lt;/p&gt;

&lt;p&gt;This classification principle extends to how you think about monitoring and alerting in production as well. Recoverable errors are business metrics. Tracking how many users tried to register with duplicate emails this week tells you something about your user experience and might suggest you need clearer messaging or better duplicate detection. Programming errors, on the other hand, are incidents that need immediate investigation. If you're seeing ORA-01722 in production, someone needs to page an engineer because something in your validation layer has failed.&lt;/p&gt;

&lt;p&gt;The practical application of this thinking changes how you structure your error handling code. Instead of wrapping entire sections of code in broad try-catch blocks, you write targeted exception handlers for the specific, expected error conditions, and you let unexpected errors propagate naturally. This approach gives you graceful handling for normal error conditions while maintaining visibility into actual bugs. Oracle's error code system makes this pattern easy to implement because each error has a distinct, meaningful identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development Errors as Production Insurance
&lt;/h2&gt;

&lt;p&gt;The frequency and timing of errors during development directly predict your production stability, but not in the way most developers think. Here's the counterintuitive truth: more errors during development often means fewer problems in production. When you're constantly hitting constraint violations, type mismatches, and validation errors while writing code, it means your defensive layers are working. Each error that fires in development is a potential production incident that will never happen.&lt;/p&gt;

&lt;p&gt;Think carefully about what it means when you write code and Oracle doesn't throw errors. Either your code is perfect (unlikely), or your validation is too permissive. The most dangerous development experience is when everything "just works" without any constraint violations. This often means you don't have enough constraints defined, and the problems are being deferred until production when real users with real data hit edge cases you never tested.&lt;/p&gt;

&lt;p&gt;Consider a concrete example. Imagine you're building a feature that processes financial transactions. In development with your clean test data, everything works smoothly. No errors at all. You deploy to production and suddenly you're getting data integrity issues because real-world amounts can be negative, null, have trailing spaces, or be formatted in ways your test data never was. Users in different locales are sending currency amounts with comma decimal separators instead of periods. Transaction timestamps are coming in with unexpected timezone offsets. None of these scenarios appeared in your sanitized test data.&lt;/p&gt;

&lt;p&gt;If you had stricter constraints and validation in place, you would have hit errors during development the moment you tried to insert your first test transaction. Oracle would have rejected negative amounts if you had a CHECK constraint requiring positive values. It would have rejected null amounts if you had a NOT NULL constraint. These early errors would have forced you to think through all these cases before deployment, to write validation logic that handles different number formats, to decide on a clear policy for how your application handles edge cases. Instead, your lack of errors in development was actually hiding the absence of proper validation.&lt;/p&gt;

&lt;p&gt;The timing of errors also matters enormously. Errors caught during unit tests cost you maybe a minute to fix. Errors caught during integration tests might cost an hour because you need to understand how multiple components interact. Errors caught during staging deployment might cost a day because you need to coordinate with other teams and re-test everything. Errors discovered in production cost exponentially more because now you're dealing with real user data, potential data corruption, customer support issues, and emergency fixes that bypass your normal development process. Oracle's immediate, explicit errors push problems as far left in the development cycle as possible.&lt;/p&gt;

&lt;p&gt;This principle should fundamentally change how you think about setting up your development environment. Your development database should be stricter than production if anything. Turn on every constraint you can think of. Use the most restrictive validation rules. Make your development environment the place where problems surface immediately and obviously. Some teams do the opposite, running with relaxed constraints in development to make things "easier" and then tightening up for production. This is backwards. You want problems to be impossible to miss during development, when fixing them is cheap and low-stakes.&lt;/p&gt;

&lt;p&gt;The best development teams measure themselves not by how few errors they see in development, but by how many errors they catch before code reaches production. A healthy development process involves constantly running into constraint violations and validation errors, fixing them immediately, and building up layers of defense that prevent entire categories of problems. When you can deploy code to production and be confident it won't hit data integrity errors, it's not because your code is magically perfect. It's because you've already hit and fixed every error condition during development.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cost of Retrofitting Strictness
&lt;/h2&gt;

&lt;p&gt;One of the most painful lessons teams learn is what happens when they try to add constraints to existing systems with dirty data. This scenario plays out constantly in organizations that started without proper validation and are now trying to improve their data quality. When you attempt to add a NOT NULL constraint to a column that already contains null values, Oracle refuses with a clear error. When you try to add a unique constraint to data that has duplicates, Oracle tells you exactly how many rows violate the constraint.&lt;/p&gt;

&lt;p&gt;At first, this feels like Oracle being difficult. You know you need this constraint for future data integrity, so why won't the database just add it? But Oracle is actually protecting you from making a bad situation worse. If it allowed you to add a constraint that existing data violates, you'd have inconsistent enforcement. New rows would be subject to the constraint while old rows would continue to violate it. Your application code would become a minefield of special cases, trying to handle records that shouldn't exist according to your current business rules.&lt;/p&gt;

&lt;p&gt;The error forces you to confront your data quality problems head-on. You need to decide: do you delete the violating rows? Update them to valid values? Create a migration strategy that grandfathers in old data under different rules? These are hard questions, but they're questions you need to answer anyway. Oracle's refusal to paper over the problem ensures you make these decisions consciously and deliberately rather than ignoring them.&lt;/p&gt;

&lt;p&gt;Many teams discover years of accumulated data quality issues when they try to add their first constraints. They find customer records with no email addresses, orders with no shipping addresses, transactions with invalid status codes, or relationships that violate referential integrity. Each of these discoveries represents a category of bug that has been silently corrupting data for months or years. The pain of cleaning up this data is real, but it's nothing compared to the alternative of continuing to accumulate bad data indefinitely.&lt;/p&gt;

&lt;p&gt;This reinforces a key insight: it's far cheaper to build systems with proper constraints from the start than to retrofit them later. Every project that begins without proper validation, thinking they'll "add it later when we have time," is building technical debt that compounds with interest. The longer you wait, the more dirty data accumulates, and the more expensive the cleanup becomes. By the time teams realize they need constraints, they often have millions of rows of questionable data and no easy way to fix it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Error Messages as Developer Experience
&lt;/h2&gt;

&lt;p&gt;The quality of error messages is actually a form of developer tooling, though it's rarely discussed in those terms. Oracle's structured error codes create a shared vocabulary across teams and even across companies. When someone asks for help with ORA-02291, experienced developers immediately know they're dealing with a foreign key constraint issue without needing additional context. They know to look for a child record referencing a non-existent parent, or a delete operation trying to remove a parent that still has children.&lt;/p&gt;

&lt;p&gt;This searchability and recognizability makes errors much less frustrating than vague messages. When you see "operation failed" or "unexpected error occurred," you have nowhere to start. You can't Google it effectively because these phrases are too generic. You can't ask colleagues for help because there's no specific identifier to reference. You're stuck digging through logs, adding debug statements, and trying to reproduce the problem with more verbose error output.&lt;/p&gt;

&lt;p&gt;Compare this to seeing ORA-01400. You can immediately search for that specific code and find documentation explaining it means "cannot insert NULL into column." You can find Stack Overflow discussions of common causes. You can search your own codebase for other places where you've handled this error. The error code serves as a handle that lets you grab onto the problem and manipulate it. It transforms a frustrating "something's wrong" into a specific, actionable diagnosis.&lt;/p&gt;

&lt;p&gt;This shared vocabulary also improves team communication and knowledge transfer. When you write in a pull request comment "this change fixes the ORA-01722 we were seeing in the import job," everyone on the team knows you're talking about invalid number conversion. The error code carries precise meaning without requiring lengthy explanation. New team members learn these codes quickly because they encounter them in discussions, documentation, and error handling code. The codes become part of your team's technical language.&lt;/p&gt;

&lt;p&gt;The structure of error messages matters too. Oracle errors typically include not just the code but the specific object that caused the problem. ORA-01400 will tell you which table and which column rejected the NULL value. ORA-02291 will tell you which foreign key constraint failed. This specificity turns errors into debugging shortcuts. You don't need to guess which of your twelve database calls caused the problem; the error points directly to the source.&lt;/p&gt;

&lt;p&gt;This principle applies beyond Oracle as well. Any system that provides structured, specific, searchable error codes is giving you better developer experience than systems with generic error messages. The investment in creating a good error code system pays dividends every time a developer encounters a problem, because it reduces the time from "something's wrong" to "I know exactly what's wrong and how to fix it."&lt;/p&gt;

&lt;h2&gt;
  
  
  The Principles: Building Systems That Fail Well
&lt;/h2&gt;

&lt;p&gt;All of this points to a set of core principles for building reliable systems. First, prefer precise failure modes over accepting questionable input. Use database constraints, schema validation, and type systems to define exactly what valid data looks like, and reject everything else. This doesn't mean being unnecessarily rigid. It means being explicit about what your system can handle.&lt;/p&gt;

&lt;p&gt;Second, fail early and loudly in development. The later an error is discovered, the more expensive it is to fix. Configure your development environment to be stricter than production, if anything, so that problems surface when they're cheap to address. This is why Oracle's behavior of throwing errors for ambiguous references is actually a feature: it catches potential issues before they become actual problems.&lt;/p&gt;

&lt;p&gt;Third, distinguish between errors you can handle and errors that indicate bugs. Build explicit exception handling for recoverable error conditions, but let programming errors bubble up and break things. The visibility of errors is crucial for maintaining code quality over time.&lt;/p&gt;

&lt;p&gt;Fourth, treat errors as learning opportunities and testing guidance. Every time you encounter an error that catches a real problem, that's an opportunity to prevent the same problem from happening again. Write a test that verifies the constraint works. Add monitoring that alerts if similar issues start occurring in production. The goal is to make errors progressively less surprising by systematically eliminating their causes.&lt;/p&gt;

&lt;p&gt;Finally, invest in good error messages and error codes. Clear, specific, searchable error identifiers make debugging faster and knowledge sharing easier. They're not just nice to have; they're essential infrastructure for maintaining complex systems.&lt;/p&gt;

&lt;h2&gt;
  
  
  Embracing the Error-Driven Development Mindset
&lt;/h2&gt;

&lt;p&gt;The shift from seeing errors as annoyances to seeing them as quality signals requires a change in mindset. When Oracle throws an ORA error, that's not the database being difficult. It's the database protecting you from yourself. When Pydantic rejects your payload, that's not the framework being pedantic. It's catching a potential bug before it can cause damage.&lt;/p&gt;

&lt;p&gt;Enterprise systems like Oracle were built to protect data and give developers high-signal diagnostics because the cost of data problems in enterprise contexts is enormous. But these principles apply just as much to smaller systems and modern web applications. The worst errors are the quiet ones that let bad state sneak into production. The best errors are explicit, precise, and actionable. They save you time by telling you exactly what to fix.&lt;/p&gt;

&lt;p&gt;The next time you see an ORA error, take a moment to appreciate what it's telling you. The error isn't the problem. It's the solution to a problem you didn't know you had yet. And that's exactly the kind of help you want from your tools.&lt;/p&gt;




&lt;p&gt;Cover photo by &lt;a href="https://unsplash.com/@brett_jordan?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Brett Jordan&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/brown-wooden-blocks-on-white-surface-XWar9MbNGUY?utm_source=unsplash&amp;amp;utm_medium=referral&amp;amp;utm_content=creditCopyText" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>database</category>
      <category>python</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Oracle 23ai's Phantom Vector Memory: A Troubleshooting Guide</title>
      <dc:creator>Smyekh David-West</dc:creator>
      <pubDate>Wed, 17 Dec 2025 07:41:38 +0000</pubDate>
      <link>https://dev.to/smyekh/oracle-23ais-phantom-vector-memory-a-troubleshooting-guide-5a1j</link>
      <guid>https://dev.to/smyekh/oracle-23ais-phantom-vector-memory-a-troubleshooting-guide-5a1j</guid>
      <description>&lt;h2&gt;
  
  
  ☁️ Pre-Flight Checklist
&lt;/h2&gt;

&lt;p&gt;Before we taxi down the runway, here’s your flight plan. Keep this handy to navigate your flight path. Welcome aboard the cloud!&lt;/p&gt;

&lt;h3&gt;
  
  
  🌥️ Takeoff
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Prerequisites &lt;/li&gt;
&lt;li&gt;A Quick Primer on Oracle Architecture&lt;/li&gt;
&lt;li&gt;The Mission: Allocate Vector Memory&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⛅️ Cruising Altitude
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The First Attempt: &lt;code&gt;ALTER SYSTEM&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The Roadblock: A Two-Layer Problem&lt;/li&gt;
&lt;li&gt;The Breakthrough: The &lt;code&gt;docker exec&lt;/code&gt; Recovery&lt;/li&gt;
&lt;li&gt;The Proof: Trust the Startup Log, Not the Parameter&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌤️ Landing &amp;amp; Taxi
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Understanding Oracle Parameter and Memory Views&lt;/li&gt;
&lt;li&gt;Recovery Cheat Sheet&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;li&gt;The "IAM Policy": Why Our &lt;code&gt;dev&lt;/code&gt; User Needed &lt;code&gt;GRANT&lt;/code&gt;s&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enjoy your flight! ☁️&lt;/p&gt;




&lt;p&gt;As part of my work with Oracle Cloud Infrastructure (OCI) and its powerful database features, I've been diving into the new AI Vector Search capabilities in Oracle Database 23ai. The free containerized version is the perfect lab for this. &lt;/p&gt;

&lt;p&gt;To get started with high-performance HNSW indexes, you need to allocate a dedicated memory pool by setting the &lt;code&gt;vector_memory_size&lt;/code&gt; parameter.&lt;/p&gt;

&lt;p&gt;A single SQL command and a restart led me down a rabbit hole of cryptic errors, misleading outputs, and a locked-out database. It turned into a fantastic troubleshooting session that taught me some valuable lessons about the inner workings of Oracle's multitenant architecture, especially in the Free edition.&lt;/p&gt;

&lt;p&gt;If you've found yourself staring at a &lt;code&gt;vector_memory_size&lt;/code&gt; that stubbornly reads &lt;code&gt;0&lt;/code&gt; or fighting the dreaded &lt;code&gt;ORA-12514&lt;/code&gt; error, this log of my troubleshooting journey is for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;This article assumes you have already successfully installed and are running the Oracle 23ai Free container, as detailed in the &lt;a href="https://dev.to/smyekh/no-more-vms-run-oracle-database-23ai-natively-on-apple-silicon-in-seconds-5dof"&gt;"The Ultimate Guide to Oracle 23ai on Apple Silicon."&lt;/a&gt;. This article begins where that one left off. &lt;/p&gt;

&lt;p&gt;This guide assumes you have the environment from Part 1 running. If you haven't set up your &lt;code&gt;dev&lt;/code&gt; user yet, pause here and go back.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Quick Primer on Oracle Architecture
&lt;/h2&gt;

&lt;p&gt;Before we dive into the troubleshooting, let's clarify a few key Oracle terms. Understanding these concepts is crucial to grasping why the problem occurs and how the solution works.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CDB (Container Database):&lt;/strong&gt; Think of this as the master database that manages the overall instance. In the free container, its name is &lt;code&gt;FREE&lt;/code&gt;. You rarely connect here for development, but it's where major configuration, like memory allocation, is ultimately managed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PDB (Pluggable Database):&lt;/strong&gt; This is an isolated, independent database that runs inside the CDB. For all application application development, you connect to the PDB. In our case, its name is &lt;code&gt;freepdb1&lt;/code&gt;. The &lt;code&gt;ORA-12514&lt;/code&gt; error happens because this PDB isn't available when the main CDB instance is down.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SGA (System Global Area):&lt;/strong&gt; This is the shared memory region that a database instance uses to store data and control information. When we set &lt;code&gt;vector_memory_size&lt;/code&gt;, we are telling Oracle to carve out a piece of the SGA specifically for vector indexes. The &lt;code&gt;STARTUP&lt;/code&gt; log shows the &lt;em&gt;actual&lt;/em&gt; composition of the SGA, which is why it's more reliable than the &lt;code&gt;SHOW PARAMETER&lt;/code&gt; command.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Mission: Allocate Vector Memory
&lt;/h2&gt;

&lt;p&gt;The first step to using AI Vector Search is to tell Oracle to set aside a dedicated chunk of RAM for it by setting the &lt;code&gt;vector_memory_size&lt;/code&gt; parameter.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🌩️ Note:&lt;/strong&gt; The 23ai Free edition is limited to 2 GB of RAM. Dedicating too much memory to vectors can starve other critical database processes. A setting of &lt;code&gt;1G&lt;/code&gt; (50% of total memory) is risky. It's wiser to start with a more conservative value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I decided to start with &lt;code&gt;500M&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The First Attempt: &lt;code&gt;ALTER SYSTEM&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Following the documentation, I connected using my &lt;code&gt;sql-sys&lt;/code&gt; &lt;em&gt;alias&lt;/em&gt; and ran the standard &lt;code&gt;ALTER SYSTEM&lt;/code&gt; command. Using &lt;code&gt;SCOPE=SPFILE&lt;/code&gt; is the robust way to make a configuration change persist across restarts.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Connect using my sql-sys alias or as SYS&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;SYSTEM&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;vector_memory_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="k"&gt;SCOPE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;SPFILE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system responded with a reassuring &lt;code&gt;System altered.&lt;/code&gt;. To apply the change, a database restart is required. The easiest way with Docker is to restart the container itself.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker restart oracle-free
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is where the journey should have ended. But instead, it's where the trouble began.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Roadblock: A Two-Layer Problem
&lt;/h2&gt;

&lt;p&gt;After the container restarts, a developer's natural first step is to connect as their day-to-day dev user and check if the vector_memory_size parameter was applied. This is where we hit our first wall: a permissions layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;-- Connecting as the dev user from Part 1
SQL&amp;gt; show parameter vector_memory_size;

ORA-00942: table or view does not exist
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is our first critical lesson. Our &lt;code&gt;dev&lt;/code&gt; user, with basic &lt;code&gt;CONNECT&lt;/code&gt; and &lt;code&gt;RESOURCE&lt;/code&gt; roles, can create tables but cannot see instance-level configuration. This is a security feature, not a bug. To investigate further, we must switch to our administrative &lt;code&gt;SYS&lt;/code&gt;user.&lt;/p&gt;

&lt;p&gt;Now, continuing as &lt;code&gt;SYS&lt;/code&gt;, we can properly check the parameter. However, this reveals the second part of our problem: a configuration layer.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;-- Now connected as SYS (e.g., via sql-sys alias)
SQL&amp;gt; SHOW PARAMETER vector_memory_size;

NAME                   TYPE        VALUE
&lt;span class="p"&gt;---------------------- ----------- ------------------------------&lt;/span&gt;
vector_memory_size     big integer 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It was still zero. My change seemed to have vanished. To understand why, I ran some diagnostic checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--1ST CHECK
SQL&amp;gt; SHOW CON_NAME;

CON_NAME
------
FREEPDB1

--2ND CHECK
SQL&amp;gt;SELECT NAME, ISPDB_MODIFIABLE
FROM V$SYSTEM_PARAMETER
WHERE NAME = 'vector_memory_size';

NAME
---
ISPDB
---
vector_memory_size
TRUE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Interpretation:&lt;/strong&gt; I was connected in the PDB (&lt;code&gt;FREEPDB1&lt;/code&gt;), and the parameter is indeed PDB-modifiable. Yet, the &lt;code&gt;SHOW PARAMETER&lt;/code&gt; command still reported &lt;code&gt;0&lt;/code&gt;. This indicated a discrepancy between the stored parameter value and the actual runtime allocation, a common quirk in the Oracle Free edition.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🌩️ A Note on the Permissions Fix&lt;br&gt;
Before proceeding, let's quickly fix the issue for our dev user. As &lt;code&gt;SYS&lt;/code&gt;, run the following &lt;code&gt;GRANT&lt;/code&gt; statements:&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GRANT SELECT ON SYS.V_$PARAMETER TO dev;
GRANT SELECT ON SYS.V_$SYSTEM_PARAMETER TO dev;
GRANT CREATE INDEX TO dev;
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;This is necessary because commands like &lt;code&gt;SHOW PARAMETER&lt;/code&gt; query special, protected data dictionary views (e.g., &lt;code&gt;V$PARAMETER&lt;/code&gt;) that are owned by the &lt;code&gt;SYS&lt;/code&gt; user. For security and stability, non-administrative users like our &lt;code&gt;dev&lt;/code&gt; account do not have permission to see this instance-wide configuration by default.&lt;/p&gt;

&lt;p&gt;These &lt;code&gt;GRANT&lt;/code&gt; statements provide our developer with the specific, read-only access they need to do their job, without making them an administrator.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;My investigation into CDB vs. PDB settings led me to try applying the change at the root and performing a full &lt;code&gt;SHUTDOWN&lt;/code&gt;/&lt;code&gt;STARTUP&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This led to a catastrophic failure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Database closed.
Database dismounted.
ORACLE instance shut down.
ERROR:
ORA-12514: Cannot connect to database. Service
freepdb1 is not registered with
the listener at host 127.0.0.1 port 1521.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I was locked out. Trying to reconnect with my &lt;code&gt;sql-sys&lt;/code&gt; alias failed with the same &lt;code&gt;ORA-12514&lt;/code&gt; error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;gt;&amp;gt;&amp;gt; sql-sys 

Copyright (c) 1982, 2025, Oracle. All rights reserved. 
ERROR: ORA-12514: Cannot connect to database. 
Service freepdb1 is not registered with the listener at host 127.0.0.1 port 1521. 
(CONNECTION_ID=Qgik7CZCByPgYwIAEaywBw==) 
Help: https://docs.oracle.com/error-help/db/ora-12514/ 

Enter user-name:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick check of the Docker container status confirmed the issue:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;docker ps

STATUS
Up 19 minutes (unhealthy)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command showed the &lt;code&gt;oracle-free&lt;/code&gt; container as &lt;code&gt;(unhealthy)&lt;/code&gt;, further indicating that the database instance inside was not fully operational.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🌩️ What &lt;code&gt;ORA-12514&lt;/code&gt; Really Means&lt;/strong&gt;&lt;br&gt;
This error means the listener is running, but the database service you're asking for (&lt;code&gt;freepdb1&lt;/code&gt;) hasn't registered with it. This happens when the main database instance is down or the Pluggable Database (PDB) is closed. In my case, the entire instance was down because PDBs do not auto-open by default in the Free edition after a CDB restart. Therefore, &lt;code&gt;FREEPDB1&lt;/code&gt; remained closed, leading to its listener unregistration.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Did you know?&lt;/strong&gt; Remember how we restarted the container in Part 1? That actually closes our PDB, which is why we need to manually open it here.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The Breakthrough: The &lt;code&gt;docker exec&lt;/code&gt; Recovery
&lt;/h2&gt;

&lt;p&gt;This is the critical lesson for anyone running Oracle in Docker. When you &lt;code&gt;SHUTDOWN&lt;/code&gt; the instance from within a SQL session, you can't just run &lt;code&gt;STARTUP&lt;/code&gt; because your client is no longer connected to anything. I encountered this with the &lt;code&gt;SP2-0640: Not connected&lt;/code&gt; error when attempting &lt;code&gt;STARTUP&lt;/code&gt; after the shutdown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You must re-enter the container and start the instance as &lt;code&gt;SYSDBA&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Exec into the container with &lt;code&gt;sqlplus&lt;/code&gt; as &lt;code&gt;sysdba&lt;/code&gt;:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; oracle-free sqlplus / as sysdba
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;You'll be greeted with a message that tells you everything you need to know:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Connected to an idle instance.
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start the database:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;STARTUP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Proof: Trust the Startup Log, Not the Parameter
&lt;/h2&gt;

&lt;p&gt;This is the moment of truth. The output that followed was the breakthrough:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total System Global Area 1603373280 bytes
Fixed Size               5007584 bytes
Variable Size            352321536 bytes
Database Buffers         704643072 bytes
Redo Buffers             4530176 bytes
Vector Memory Area       536870912 bytes  &amp;lt;-- VOILA!
Database mounted.
Database opened.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it was: &lt;strong&gt;&lt;code&gt;Vector Memory Area 536870912 bytes&lt;/code&gt;&lt;/strong&gt;. That's our 500M!&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🌩️ The Deceptive Parameter&lt;/strong&gt;&lt;br&gt;
My &lt;code&gt;ALTER SYSTEM&lt;/code&gt; command &lt;em&gt;had&lt;/em&gt; worked from the beginning. The &lt;code&gt;SHOW PARAMETER&lt;/code&gt; view in the PDB is misleading for this setting in the Free edition. The startup log and &lt;code&gt;V$VECTOR_MEMORY_POOL&lt;/code&gt; are the authoritative sources. &lt;strong&gt;Trust the startup log and &lt;code&gt;V$VECTOR_MEMORY_POOL&lt;/code&gt; as the authoritative sources!&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Final Steps: Bringing the Database Fully Online
&lt;/h3&gt;

&lt;p&gt;The instance was up, but the PDB was still closed. You must open it manually.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 1. Open the Pluggable Database&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;PLUGGABLE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;OPEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. Verify the PDB Status&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;open_mode&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;pdbs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;-- You should see FREEPDB1 in READ WRITE mode&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. Switch into the PDB for your work&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;SESSION&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;CONTAINER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FREEPDB1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. Final verification of vector memory pool allocation within the PDB&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alloc_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;used_bytes&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;VECTOR_MEMORY_POOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 5. Verify total SGA and vector memory allocation&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;SGA&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Expected Outputs for Step 4 &amp;amp; 5:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;-- STEP4
POOL         ALLOC_BYTES   USED_BYTES
&lt;span class="p"&gt;------------ ------------  ----------&lt;/span&gt;
1MB POOL     469762048     0
64KB POOL     50331648     0

-- STEP 5
Total System Global Area 1603373280 bytes
Fixed Size                  5007584 bytes
Variable Size             520093696 bytes
Database Buffers          536870912 bytes
Redo Buffers                4530176 bytes
Vector Memory Area        536870912 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Explanation:
&lt;/h4&gt;

&lt;ul&gt;
&lt;li&gt;Each pool manages a specific block size of vector memory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ALLOC_BYTES&lt;/code&gt; shows total allocated memory for that pool.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;USED_BYTES&lt;/code&gt; shows how much memory is currently in use. &lt;code&gt;0&lt;/code&gt; usage indicates the system is ready but no active vector operations are running yet.&lt;/li&gt;
&lt;li&gt;The “Vector Memory Area” line appears because of the &lt;code&gt;ALTER SYSTEM SET vector_memory_size=500M SCOPE=SPFILE;&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;It confirms that ~512 MB (close to your configured 500 MB) was successfully allocated on startup.&lt;/li&gt;
&lt;li&gt;This memory is used for AI Vector Search features like storing vector embeddings and building vector indexes.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;ALLOC_BYTES&lt;/code&gt; in &lt;strong&gt;Step 4&lt;/strong&gt; (~520 MB combined) matches this total, confirming your vector memory configuration is active and healthy.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&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%2Fu1pfxky5gs51rbiahe2u.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%2Fu1pfxky5gs51rbiahe2u.png" alt="Final Steps" width="800" height="480"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding Oracle Parameter and Memory Views
&lt;/h2&gt;

&lt;p&gt;Oracle provides several ways to inspect configuration parameters and memory allocation, but their scope and reliability can differ. Understanding these views is crucial for accurate troubleshooting.&lt;/p&gt;

&lt;p&gt;First, a quick note on Oracle's &lt;code&gt;V$&lt;/code&gt; views: these are &lt;strong&gt;dynamic performance views&lt;/strong&gt; that provide access to current performance, resource usage, and session activity information. The &lt;code&gt;V&lt;/code&gt; typically stands for "view," and the &lt;code&gt;$&lt;/code&gt; is part of Oracle's naming convention for these special views, which are often built on top of underlying &lt;code&gt;X$&lt;/code&gt; tables. They give you a real-time snapshot of the database instance's state.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;SHOW PARAMETER &amp;lt;parameter_name&amp;gt;&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This SQL*Plus command is a convenient way to quickly check a parameter's value. However, as we saw with &lt;code&gt;vector_memory_size&lt;/code&gt; in the Free edition, it can sometimes report a default or &lt;code&gt;0&lt;/code&gt; even when the parameter is actively allocated at runtime. Its output often reflects the value stored in the SPFILE or a default, not necessarily the &lt;em&gt;currently active&lt;/em&gt; runtime value, especially for dynamically allocated memory components.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;V$SYSTEM_PARAMETER&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This dynamic performance view shows the current system-wide parameter values, typically reflecting what's in the SPFILE or the last &lt;code&gt;ALTER SYSTEM&lt;/code&gt; command. It's useful for understanding the &lt;em&gt;intended&lt;/em&gt; configuration. The &lt;code&gt;ISPDB_MODIFIABLE&lt;/code&gt; column indicates if a parameter can be changed at the PDB level.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;V$PARAMETER&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Similar to &lt;code&gt;V$SYSTEM_PARAMETER&lt;/code&gt;, but it shows parameter values for the &lt;em&gt;current session&lt;/em&gt;. For system-level parameters like &lt;code&gt;vector_memory_size&lt;/code&gt;, its value will often mirror &lt;code&gt;V$SYSTEM_PARAMETER&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;V$SGA&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This view provides a breakdown of the System Global Area (SGA) components and their sizes. After a successful &lt;code&gt;STARTUP&lt;/code&gt; with &lt;code&gt;vector_memory_size&lt;/code&gt; configured, you should see &lt;code&gt;Vector Memory Area&lt;/code&gt; listed here with its allocated size. This is a reliable source for confirming the actual memory allocation.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SQL&amp;gt; SHOW SGA;
Example Output (excerpt):
Total System Global Area 1603373280 bytes
Fixed Size                  5007584 bytes
Variable Size             352321536 bytes
Database Buffers          704643072 bytes
Redo Buffers                4530176 bytes
Vector Memory Area        536870912 bytes  &amp;lt;--Confirms allocation within SGA 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This output directly correlates with the &lt;code&gt;STARTUP&lt;/code&gt; log and visually confirms that the &lt;code&gt;Vector Memory Area&lt;/code&gt; has been carved out of the total SGA. Remember, in the Oracle Free edition, the total SGA is limited, so increasing one component might implicitly reduce others if &lt;code&gt;SGA_TARGET&lt;/code&gt; is constrained.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;V$VECTOR_MEMORY_POOL&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;This view is specifically designed to show the details of the vector memory pool. It's the most direct and authoritative source for confirming the active vector memory allocation. If this view shows your configured size, you can be confident that vector search memory is active.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alloc_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;used_bytes&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;VECTOR_MEMORY_POOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Recovery Cheat Sheet
&lt;/h2&gt;

&lt;p&gt;If you get locked out with &lt;code&gt;ORA-12514&lt;/code&gt;, here is your recovery sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. (Optional) Check Docker container status - it will likely be unhealthy&lt;/span&gt;
docker ps

&lt;span class="c"&gt;# 2. Connect to the idle instance from your host terminal&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; oracle-free sqlplus / as sysdba
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then, inside the &lt;code&gt;SQL&amp;gt;&lt;/code&gt; prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- 3. Start the database instance&lt;/span&gt;
&lt;span class="n"&gt;STARTUP&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. Open the pluggable databases&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="n"&gt;PLUGGABLE&lt;/span&gt; &lt;span class="k"&gt;DATABASE&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt; &lt;span class="k"&gt;OPEN&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 5. (Optional) Switch to your PDB to continue work&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;SESSION&lt;/span&gt; &lt;span class="k"&gt;SET&lt;/span&gt; &lt;span class="n"&gt;CONTAINER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FREEPDB1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;--6. (Optional) Verify vector memory pool allocation&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alloc_bytes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;used_bytes&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;V&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;VECTOR_MEMORY_POOL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 7. (Optional) Verify overall SGA breakdown&lt;/span&gt;
&lt;span class="k"&gt;SHOW&lt;/span&gt; &lt;span class="n"&gt;SGA&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the database fully online and vector memory confirmed, I was finally ready to get back to building.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🌩️ A Note on Privileges:&lt;/strong&gt; It's important to note that this entire troubleshooting process was done with &lt;code&gt;SYSDBA&lt;/code&gt; privileges. This is a prime example of the principle of least privilege in action. My day-to-day &lt;code&gt;dev&lt;/code&gt; user rightfully lacks the permissions to alter system parameters or restart the database. Using the superuser for these administrative tasks is not only necessary but is also a validation of a secure and well-designed workflow, separating experimental and operational access models.&lt;/p&gt;
&lt;/blockquote&gt;

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

&lt;p&gt;The key takeaway is to &lt;strong&gt;trust the startup log, &lt;code&gt;V$SGA&lt;/code&gt;, and &lt;code&gt;V$VECTOR_MEMORY_POOL&lt;/code&gt;&lt;/strong&gt;. These provide the ground truth of what's happening in the SGA. While &lt;code&gt;SHOW PARAMETER&lt;/code&gt; and &lt;code&gt;V$SYSTEM_PARAMETER&lt;/code&gt; are useful for intended configuration, the startup sequence and direct memory pool queries reveal the real story of runtime allocation. By understanding the roles of the CDB and PDB and mastering the &lt;code&gt;docker exec&lt;/code&gt; recovery flow, you can confidently manage your Oracle instance and unlock its most powerful features. &lt;/p&gt;

&lt;p&gt;Happy building and vectorizing! ☁️&lt;/p&gt;




&lt;h2&gt;
  
  
  The "IAM Policy": Why Our &lt;code&gt;dev&lt;/code&gt; User Needs &lt;code&gt;GRANT&lt;/code&gt;s
&lt;/h2&gt;

&lt;p&gt;In a cloud environment like OCI, a Cloud Architect never gives a developer or service full administrative access. Instead, they create fine-grained &lt;strong&gt;IAM (Identity and Access Management) policies&lt;/strong&gt;. An IAM policy explicitly states &lt;em&gt;who&lt;/em&gt; can do &lt;em&gt;what&lt;/em&gt; on &lt;em&gt;which&lt;/em&gt; resources.&lt;/p&gt;

&lt;p&gt;Our &lt;code&gt;GRANT&lt;/code&gt; commands in Part 2 were the database equivalent of an IAM policy. We encountered an &lt;code&gt;ORA-00942&lt;/code&gt; error because our &lt;code&gt;dev&lt;/code&gt; user, by default, had no policy allowing it to view the system's configuration.&lt;/p&gt;

&lt;p&gt;The fix was not to grant the &lt;code&gt;DBA&lt;/code&gt; role, but to create a precise policy:&lt;br&gt;
&lt;code&gt;GRANT SELECT ON SYS.V_$PARAMETER TO dev;&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is the Principle of Least Privilege in action, and it's the most important concept in cloud security.&lt;/p&gt;
&lt;h3&gt;
  
  
  Object vs. System Privileges: A Cloud Analogy
&lt;/h3&gt;

&lt;p&gt;Oracle's security model, which separates &lt;strong&gt;Object Privileges&lt;/strong&gt; (acting on a table) from &lt;strong&gt;System Privileges&lt;/strong&gt; (acting on the whole database), mirrors the separation of concerns in OCI.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Object Privileges&lt;/strong&gt; are like permissions on a specific OCI resource (e.g., writing to a specific Object Storage bucket). The &lt;code&gt;RESOURCE&lt;/code&gt; role lets you create your own resources.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;System Privileges&lt;/strong&gt; are like tenancy-level permissions (e.g., the ability to launch new Compute instances or view network configurations).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our &lt;code&gt;dev&lt;/code&gt; user could manage its own "resources" (tables) but needed a specific policy (&lt;code&gt;GRANT&lt;/code&gt;) to view "tenancy-level" configuration (&lt;code&gt;V$PARAMETER&lt;/code&gt;).&lt;/p&gt;
&lt;h3&gt;
  
  
  Anatomy of a Secure Developer Role
&lt;/h3&gt;

&lt;p&gt;This is the final "IAM policy" for our developer, enabling them to build AI applications securely.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- These SQL commands should be executed once as the SYS user&lt;/span&gt;
&lt;span class="c1"&gt;-- in your SQL client (e.g., SQLcl, SQL Developer, or via 'docker exec -it oracle-free sqlplus / as sysdba').&lt;/span&gt;
&lt;span class="c1"&gt;-- Ensure you are connected to the FREEPDB1 container if running from SYS.&lt;/span&gt;

&lt;span class="c1"&gt;-- 1. The right to log in (authenticate).&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 2. A role to create their own resources (tables, views).&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="n"&gt;RESOURCE&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 3. The right to use storage space (a quota on a resource).&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt; &lt;span class="n"&gt;QUOTA&lt;/span&gt; &lt;span class="n"&gt;UNLIMITED&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;dev_data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 4. The policy to inspect specific system configurations.&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;SYS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;V_&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="k"&gt;PARAMETER&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;SYS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;V_&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;SYSTEM_PARAMETER&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- 5. The policy to create a specific type of resource (an index).&lt;/span&gt;
&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this secure foundation, our developer is ready to build.&lt;/p&gt;




&lt;p&gt;Cover Photo by &lt;a href="https://unsplash.com/@boliviainteligente?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;BoliviaInteligente&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/text-_BFJOg1nxaw?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>ai</category>
      <category>vectordatabase</category>
      <category>database</category>
    </item>
    <item>
      <title>No More VMs: Run Oracle Database 23ai Natively on Apple Silicon in Seconds</title>
      <dc:creator>Smyekh David-West</dc:creator>
      <pubDate>Mon, 08 Dec 2025 10:09:07 +0000</pubDate>
      <link>https://dev.to/smyekh/no-more-vms-run-oracle-database-23ai-natively-on-apple-silicon-in-seconds-5dof</link>
      <guid>https://dev.to/smyekh/no-more-vms-run-oracle-database-23ai-natively-on-apple-silicon-in-seconds-5dof</guid>
      <description>&lt;h3&gt;
  
  
  ⚡️ 30-Second Quick Start (Copy/Paste and Run)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; oracle-free &lt;span class="nt"&gt;-p&lt;/span&gt; 1521:1521 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; oracle-data:/opt/oracle/oradata &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ORACLE_PWD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OracleIsAwesome &lt;span class="se"&gt;\&lt;/span&gt;
  container-registry.oracle.com/database/free:latest-lite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This starts Oracle 23ai Free instantly — no VM, no installer, no configuration. Just run and connect.&lt;/p&gt;




&lt;h2&gt;
  
  
  ☁️ Pre-Flight Checklist
&lt;/h2&gt;

&lt;p&gt;Before we taxi down the runway, here’s your flight plan. Keep this handy to navigate your flight path. Welcome aboard the cloud!&lt;/p&gt;

&lt;h3&gt;
  
  
  🌥️ Takeoff
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Why This Changes Everything&lt;/li&gt;
&lt;li&gt;Prerequisites&lt;/li&gt;
&lt;li&gt;Get Started in 60 Seconds: The Docker Command&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⛅️ Cruising Altitude
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;4 Ways to Connect to Your New Database&lt;/li&gt;
&lt;li&gt;Managing Your Database Container&lt;/li&gt;
&lt;li&gt;Understanding Data Persistence: Your Data is Safe&lt;/li&gt;
&lt;li&gt;Best Practice: Create a Dedicated Developer User&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌤️ Landing &amp;amp; Taxi
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Why 23ai Might Be the Only Database You Need&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;li&gt;Bonus: Faster Connections with Shell Shortcuts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enjoy your flight! ☁️&lt;/p&gt;




&lt;p&gt;For years, developers on macOS have faced a common hurdle: running Oracle Database locally meant clunky VMs, slow performance, and a drain on system resources. Personally, the thought of setting up a dedicated environment or firing up my UTM Linux VM was always daunting. &lt;/p&gt;

&lt;p&gt;Thankfully, that's not the case anymore.&lt;/p&gt;

&lt;p&gt;Oracle's &lt;a href="https://www.oracle.com/database/free/get-started/#connecting" rel="noopener noreferrer"&gt;Oracle Database 23ai Free&lt;/a&gt; is available as a native ARM64 container image for Apple Silicon (M1–M4). This allows developers to run a full Oracle Database locally on macOS using Docker—no emulation, no VMs, and no workarounds. It makes Oracle feel as easy to spin up as Postgres for my FastAPI projects, and that’s a huge improvement for local development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Changes Everything
&lt;/h2&gt;

&lt;p&gt;Before this release, the developer experience was painful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;No Native Support:&lt;/strong&gt; Oracle databases required an x86 architecture, forcing Mac users to run a heavy Linux VM.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Slow and Inefficient:&lt;/strong&gt; Virtualization is resource-intensive, leading to slow startup times, high RAM usage, and significant battery drain.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Complex Setup:&lt;/strong&gt; Configuring VMs and networking was a constant source of friction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now, with the native ARM64 container, you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Significantly faster startup:&lt;/strong&gt; Running natively on Apple Silicon eliminates emulation overhead and reduces startup time to under a minute.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reduced resource usage:&lt;/strong&gt; No full VM means dramatically lower CPU/RAM consumption and smoother performance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simplified workflow:&lt;/strong&gt; It behaves like any standard Docker container—pull, run, connect, done.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;An Apple Silicon Mac:&lt;/strong&gt; (M1, M2, M3, M4, etc.)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Docker Desktop:&lt;/strong&gt; Make sure it's installed and running.&lt;/li&gt;
&lt;/ul&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%2Fq5o0c8v0hhriacoejsp9.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%2Fq5o0c8v0hhriacoejsp9.png" alt="Docker ps" width="800" height="58"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Get Started in 60 Seconds: The Docker Command
&lt;/h2&gt;

&lt;p&gt;Open your terminal and run this single command. Replace &lt;code&gt;&amp;lt;your-password&amp;gt;&lt;/code&gt; with a password of your choice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; oracle-free &lt;span class="nt"&gt;-p&lt;/span&gt; 1521:1521 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; oracle-data:/opt/oracle/oradata &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ORACLE_PWD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&amp;lt;your-password&amp;gt; &lt;span class="se"&gt;\&lt;/span&gt;
  container-registry.oracle.com/database/free:latest-lite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Example:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; oracle-free &lt;span class="nt"&gt;-p&lt;/span&gt; 1521:1521 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; oracle-data:/opt/oracle/oradata &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ORACLE_PWD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OracleIsAwesome &lt;span class="se"&gt;\&lt;/span&gt;
  container-registry.oracle.com/database/free:latest-lite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fd69hjtt4jumbkmv594ki.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%2Fd69hjtt4jumbkmv594ki.png" alt="Docker run" width="800" height="484"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  What does &lt;code&gt;-d&lt;/code&gt; do?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;-d&lt;/code&gt; flag runs the container in detached mode, meaning it runs in the background without blocking your terminal. With &lt;code&gt;-d&lt;/code&gt;, the container starts in the background and you can continue using your terminal immediately.&lt;/p&gt;

&lt;p&gt;For first-time setup, some developers prefer to see the startup logs in real-time to confirm everything is working. If that's you, omit the &lt;code&gt;-d&lt;/code&gt; flag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--name&lt;/span&gt; oracle-free &lt;span class="nt"&gt;-p&lt;/span&gt; 1521:1521 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; oracle-data:/opt/oracle/oradata &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ORACLE_PWD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OracleIsAwesome &lt;span class="se"&gt;\&lt;/span&gt;
  container-registry.oracle.com/database/free:latest-lite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without &lt;code&gt;-d&lt;/code&gt;, the container runs in the foreground and you'll see all the logs directly as the database initializes. Wait for the message DATABASE IS READY TO USE! to appear.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🌩️ Important:&lt;/strong&gt; If you run without &lt;code&gt;-d&lt;/code&gt;, pressing &lt;code&gt;Ctrl + C&lt;/code&gt; will stop the container entirely. If this happens, don't worry—just restart it in the background:&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# 1. Start the container in the background&lt;/span&gt;
docker start oracle-free

&lt;span class="c"&gt;# 2. Confirm the container is running (check the STATUS column)&lt;/span&gt;
docker ps

&lt;span class="c"&gt;# 3. Follow the logs to see when the database is ready&lt;/span&gt;
docker logs &lt;span class="nt"&gt;-f&lt;/span&gt; oracle-free
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;Once you see &lt;code&gt;DATABASE IS READY TO USE!&lt;/code&gt;, press &lt;code&gt;Ctrl + C&lt;/code&gt; to exit the log view. This time, the container will keep running in the background because it was started with &lt;code&gt;docker start&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  What does &lt;code&gt;-e ORACLE_PWD&lt;/code&gt; do?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;-e&lt;/code&gt; flag sets an environment variable inside the container. &lt;code&gt;ORACLE_PWD&lt;/code&gt; sets the initial password for the &lt;code&gt;SYS&lt;/code&gt; and &lt;code&gt;SYSTEM&lt;/code&gt; administrative users. If you omit this, the container won't start.&lt;/p&gt;

&lt;h3&gt;
  
  
  What does &lt;code&gt;-v oracle-data:/opt/oracle/oradata&lt;/code&gt; do?
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;-v&lt;/code&gt; flag is crucial for data persistence. It creates a Docker named volume called &lt;code&gt;oracle-data&lt;/code&gt; and maps it to the &lt;code&gt;/opt/oracle/oradata&lt;/code&gt; directory inside the container—the location where Oracle stores its database files.&lt;/p&gt;

&lt;p&gt;This separates your data from the container's lifecycle. If you stop, remove, or update the oracle-free container, the oracle-data volume remains safely on your system. Without this line, all data and schema changes would be lost forever when the container is removed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Checking When Your Database is Ready
&lt;/h3&gt;

&lt;p&gt;If you started the container in detached mode (with &lt;code&gt;-d&lt;/code&gt;), you can check the startup progress anytime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Follow the logs to see when the database is ready&lt;/span&gt;
docker logs &lt;span class="nt"&gt;-f&lt;/span&gt; oracle-free
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait for the logs to display the message: &lt;code&gt;DATABASE IS READY TO USE!&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Once you see this message, press &lt;code&gt;Ctrl + C&lt;/code&gt; to exit the log view. The container will keep running in the background.&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%2Fl1nnmvt92u2faot2kiq8.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%2Fl1nnmvt92u2faot2kiq8.png" alt="Docker ps" width="800" height="33"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  4 Ways to Connect to Your New Database
&lt;/h2&gt;

&lt;p&gt;Once the container is running, you can connect to it using your favorite SQL tool.&lt;/p&gt;

&lt;h3&gt;
  
  
  Your Connection Cheat Sheet
&lt;/h3&gt;

&lt;p&gt;No matter which tool you use, these are your connection details:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Host&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localhost&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Port&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1521&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Service Name&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;freepdb1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Admin Users&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;system&lt;/code&gt; (default) or &lt;code&gt;sys&lt;/code&gt; (requires &lt;code&gt;AS SYSDBA&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Password&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;The one you set (e.g., &lt;code&gt;OracleIsAwesome&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Connect String&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localhost:1521/freepdb1&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Quick Copy/Paste Connection Defaults&lt;/span&gt;
&lt;span class="nv"&gt;HOST&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;localhost
&lt;span class="nv"&gt;PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1521
&lt;span class="nv"&gt;SERVICE_NAME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;freepdb1

&lt;span class="c"&gt;# Admin users (created automatically)&lt;/span&gt;
SYSTEM / &amp;lt;your-password&amp;gt;
SYS / &amp;lt;your-password&amp;gt;  &lt;span class="o"&gt;(&lt;/span&gt;connect as SYSDBA &lt;span class="k"&gt;if &lt;/span&gt;needed&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;freepdb1&lt;/code&gt;?&lt;/strong&gt;  The Oracle container uses a Multitenant Architecture.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;CDB (Container Database - &lt;code&gt;FREE&lt;/code&gt;):&lt;/strong&gt; This is the root database that manages the overall instance. You rarely connect here for development.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;PDB (Pluggable Database - &lt;code&gt;freepdb1&lt;/code&gt;):&lt;/strong&gt; This is an isolated, independent database running inside the CDB. For all application development, you should connect to the PDB.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🌩️ Practical takeaway:&lt;/strong&gt; For all development work, always connect to &lt;code&gt;freepdb1&lt;/code&gt;, not the root container.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  A Quick Note: &lt;code&gt;SYS&lt;/code&gt;, &lt;code&gt;SYSTEM&lt;/code&gt;, and &lt;code&gt;AS SYSDBA&lt;/code&gt;
&lt;/h4&gt;

&lt;p&gt;You will often see &lt;strong&gt;AS SYSDBA&lt;/strong&gt; in Oracle documentation. Here’s a simple guide:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SYS&lt;/code&gt; User:&lt;/strong&gt; The true superuser. It owns the database's core data dictionary and can do anything. To connect as &lt;code&gt;SYS&lt;/code&gt;, you must use the &lt;code&gt;AS SYSDBA&lt;/code&gt; clause, which activates system-level privileges. It's used for fundamental operations like starting or stopping the database.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;SYSTEM&lt;/code&gt; User:&lt;/strong&gt; A default administrator account with extensive rights, but less powerful than &lt;code&gt;SYS&lt;/code&gt;. It's practical for administrative tasks like creating new users (as  shown later).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;AS SYSDBA&lt;/code&gt;:&lt;/strong&gt; This is a special privilege, not a user. It tells the database you're connecting with the highest level of authority.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🌩️ Rule of Thumb:&lt;/strong&gt; Use &lt;code&gt;SYS AS SYSDBA&lt;/code&gt; only when necessary. For creating users or granting general permissions, &lt;code&gt;SYSTEM&lt;/code&gt; is fine. For application work, always use a dedicated, non-admin user.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  1. For the CLI Fans: SQLcl
&lt;/h3&gt;

&lt;p&gt;If you prefer the command line, Oracle SQLcl is the modern tool of choice.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Install SQLcl:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;sqlcl
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add SQLcl to your PATH:&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Note: The version number (&lt;code&gt;25.3.0.274.1210&lt;/code&gt;) might be different for you. Check the correct path in &lt;code&gt;/opt/homebrew/Caskroom/sqlcl/&lt;/code&gt;.&lt;/em&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'export PATH=/opt/homebrew/Caskroom/sqlcl/25.3.0.274.1210/sqlcl/bin:$PATH'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Reload your terminal configuration:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Confirm it's working:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sql &lt;span class="nt"&gt;-v&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;Connecting with SQLcl&lt;/strong&gt;&lt;br&gt;
Once installed, connecting is a one-liner:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Connect as the default admin user &lt;/span&gt;
&lt;span class="c"&gt;# (good for creating other users)&lt;/span&gt;
sql system/OracleIsAwesome@localhost:1521/freepdb1

&lt;span class="c"&gt;# Or, connect as the superuser for powerful database operations&lt;/span&gt;
sql sys/OracleIsAwesome@localhost:1521/freepdb1 as sysdba
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fhv580z6ulwx9urbxlt8m.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%2Fhv580z6ulwx9urbxlt8m.png" alt="sql sys" width="800" height="237"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Connect Using the Built-In SQL*Plus (No Installation Needed)
&lt;/h3&gt;

&lt;p&gt;The Docker container comes with the classic SQL*Plus tool pre-installed. You can use &lt;code&gt;docker exec&lt;/code&gt; to run it directly from your Mac's terminal without installing any client software or even entering the container's shell.&lt;/p&gt;

&lt;p&gt;Run this single command on your terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# As the default admin user&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; oracle-free sqlplus system/OracleIsAwesome@localhost:1521/freepdb1

&lt;span class="c"&gt;# As the superuser&lt;/span&gt;
docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; oracle-free sqlplus sys/OracleIsAwesome@localhost:1521/freepdb1 as sysdba
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fnh5iyhvkh9c59mjqa9o6.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%2Fnh5iyhvkh9c59mjqa9o6.png" alt="docker exec" width="800" height="192"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Or, if you created your dedicated developer user:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; oracle-free sqlplus dev/DevPassword123@localhost:1521/freepdb1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command directly connects you to the database and drops you into an interactive SQL*Plus session.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. For the GUI Lovers: SQL Developer
&lt;/h3&gt;

&lt;p&gt;If you prefer a graphical interface, use the free SQL Developer.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Download:&lt;/strong&gt; &lt;a href="https://www.oracle.com/tools/downloads/sqldev-downloads.html" rel="noopener noreferrer"&gt;Oracle SQL Developer Downloads&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt; Click the &lt;strong&gt;&lt;code&gt;+&lt;/code&gt;&lt;/strong&gt; icon to create a new connection.&lt;/li&gt;
&lt;li&gt; Fill in the details from the cheat sheet above.&lt;/li&gt;
&lt;li&gt; Click &lt;strong&gt;Test&lt;/strong&gt;. If it says "Success," click &lt;strong&gt;Connect&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  4. For the VS Code Enthusiasts: Oracle SQL Developer Extension
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt; In VS Code, go to the Extensions marketplace and search for &lt;strong&gt;"Oracle SQL Developer Extension for VS Code"&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Install the official extension from Oracle.&lt;/li&gt;
&lt;li&gt; Click the Oracle Database icon in the activity bar.&lt;/li&gt;
&lt;li&gt; Click the &lt;strong&gt;&lt;code&gt;Create Connection&lt;/code&gt;&lt;/strong&gt; button.&lt;/li&gt;
&lt;li&gt; Fill in the connection details. &lt;strong&gt;Crucially, make sure you select &lt;code&gt;Service Name&lt;/code&gt; for the connection type, not &lt;code&gt;SID&lt;/code&gt;&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt; Click &lt;strong&gt;Connect&lt;/strong&gt;. You can now browse your schema and run queries directly from VS Code.&lt;/li&gt;
&lt;/ol&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%2Fnhqvf9gdhamp5brkzuf7.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%2Fnhqvf9gdhamp5brkzuf7.png" alt="VS Code Oracle DB connection settings" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  Test your Connection
&lt;/h4&gt;

&lt;p&gt;Once connected, you have a couple of ways to interact with your database directly within VS Code:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SQL Worksheet&lt;/strong&gt;: Right-click on your connection in the Oracle Database panel and select &lt;strong&gt;Open New SQL Worksheet&lt;/strong&gt;. This opens a standard editor where you can write and execute queries.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SQLcl Terminal&lt;/strong&gt;: Right-click the connection and choose &lt;strong&gt;Open SQLcl in Terminal&lt;/strong&gt;. This launches an integrated terminal running Oracle's modern command-line interface, SQLcl. You may be prompted to enter your password (e.g OracleIsAwesome).&lt;/p&gt;&lt;/li&gt;
&lt;/ol&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%2Fo05yaxot877wsmyxvx94.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%2Fo05yaxot877wsmyxvx94.png" alt="VSCODE" width="800" height="411"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To test your connection, open a worksheet or SQLcl session and run this simple query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Test your connection with a simple query!&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="s1"&gt;'Connected in VS Code!'&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;dual&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see the result directly in VS Code, confirming your setup is complete.&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%2Fsf8zql0ulyrlerbz3mmy.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%2Fsf8zql0ulyrlerbz3mmy.png" alt="VSCODE FINAL" width="800" height="200"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;When you're done, you can right-click the connection and select &lt;strong&gt;Disconnect&lt;/strong&gt;. &lt;br&gt;
The next time you want to use it, simply click the connection name again. &lt;/p&gt;

&lt;p&gt;If you didn't save your password during setup, VS Code will prompt you for it before reconnecting. However, if you checked the "Save Password" box when creating the connection, it will reconnect instantly with a single click.&lt;/p&gt;
&lt;h2&gt;
  
  
  Managing Your Database Container
&lt;/h2&gt;

&lt;p&gt;Your container, named &lt;code&gt;oracle-free&lt;/code&gt;, persists on your system. You don't need to run the &lt;code&gt;docker run&lt;/code&gt; command again.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Start the database:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;docker start oracle-free
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Stop the database:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;docker stop oracle-free
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;Check if it's running:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;docker ps
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;View live logs (useful for debugging):&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;docker logs &lt;span class="nt"&gt;-f&lt;/span&gt; oracle-free
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;li&gt;

&lt;p&gt;&lt;strong&gt;"Remove Everything" Cleanup Commands&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Completely remove the container (does not delete your data volume)&lt;/span&gt;
docker &lt;span class="nb"&gt;rm &lt;/span&gt;oracle-free

&lt;span class="c"&gt;# Remove the container and its stored data&lt;/span&gt;
docker &lt;span class="nb"&gt;rm &lt;/span&gt;oracle-free
docker volume &lt;span class="nb"&gt;rm &lt;/span&gt;oracle-data
&lt;/code&gt;&lt;/pre&gt;




&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  Understanding Data Persistence: Your Data is Safe
&lt;/h2&gt;

&lt;p&gt;One of the most common questions when working with Docker containers is: &lt;strong&gt;"What happens to my data?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The good news is that your database is safer than you think.&lt;/p&gt;

&lt;h3&gt;
  
  
  What Deletes Your Data (and What Doesn't)
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;These actions are SAFE — your data persists:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stopping the container: docker stop oracle-free&lt;/li&gt;
&lt;li&gt;Removing the container: docker rm oracle-free&lt;/li&gt;
&lt;li&gt;Restarting your Mac&lt;/li&gt;
&lt;li&gt;Updating the container to a new version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Your data will ONLY be deleted if you explicitly run:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume &lt;span class="nb"&gt;rm &lt;/span&gt;oracle-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the only command that permanently destroys your database files. Docker will even warn you if the volume is still in use.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Critical Rule: Always Attach Your Volume
&lt;/h3&gt;

&lt;p&gt;Here's something crucial to understand: when you recreate a container (after running &lt;code&gt;docker rm oracle-free&lt;/code&gt;), you must include the &lt;code&gt;-v oracle-data:/opt/oracle/oradata&lt;/code&gt; flag in your docker run command.&lt;/p&gt;

&lt;p&gt;Why? Here's the key distinction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The volume persists automatically&lt;/strong&gt; — it exists independently on your system.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The container does NOT remember it&lt;/strong&gt; — containers are stateless.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Think of it like this: Your database files are stored in a safe deposit box (&lt;code&gt;oracle-data&lt;/code&gt; volume) at the bank (Docker). When you create a new container, you need to explicitly give it the key (&lt;code&gt;-v oracle-data:/opt/oracle/oradata&lt;/code&gt;) to access that box.&lt;/p&gt;

&lt;h3&gt;
  
  
  Example: Recreating Your Container (Updates, Restarts, or Recovery)
&lt;/h3&gt;

&lt;p&gt;Whether you're updating to a newer image, recovering from an accidental deletion, or just want a fresh container, the process is the same. Your data persists through it all. Let's say you removed your container and want to start fresh:&lt;/p&gt;

&lt;h4&gt;
  
  
  Step 1: Pull the Latest Image (Optional but Recommended)
&lt;/h4&gt;

&lt;p&gt;Before recreating your container, it's a good idea to check for updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker pull container-registry.oracle.com/database/free:latest-lite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Example output when downloading a newer version:&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%2F8n0av695sd14tmpgif3e.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%2F8n0av695sd14tmpgif3e.png" alt="docker pull" width="800" height="160"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you already have the latest version, Docker will tell you:&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%2Fchdy4vti24312a6wxd26.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%2Fchdy4vti24312a6wxd26.png" alt="docker pull 2" width="800" height="76"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚡ Tip:&lt;/strong&gt; Docker keeps old images even after pulling a new latest tag. If you want to clean up the old one, run:&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;  docker image prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Step 2: Create a New Container
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create a new container with the SAME volume attached&lt;/span&gt;
docker run &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; oracle-free &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 1521:1521 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;ORACLE_PWD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;OracleIsAwesome &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; oracle-data:/opt/oracle/oradata &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; container-registry.oracle.com/database/free:latest-lite
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🌩️ Note:&lt;/strong&gt; The &lt;code&gt;-d&lt;/code&gt; flag runs the container in detached mode (in the background). Unlike the initial &lt;code&gt;docker run&lt;/code&gt; command in this guide, which attaches to your terminal and shows live logs, using &lt;code&gt;-d&lt;/code&gt; starts the container and immediately returns you to your command prompt. The container keeps running in the background, and you can view its logs anytime with &lt;code&gt;docker logs -f oracle-free&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;What happens:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Docker detects the existing oracle-data volume&lt;/li&gt;
&lt;li&gt;It reuses it (does NOT create a new one)&lt;/li&gt;
&lt;li&gt;Your database comes back &lt;strong&gt;exactly as you left it&lt;/strong&gt; — all tables, users, and data intact&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Important: Passwords Are Preserved
&lt;/h3&gt;

&lt;p&gt;When you restore from an existing volume, the &lt;code&gt;-e ORACLE_PWD&lt;/code&gt; flag is ignored. Your database already has passwords stored in the datafiles, so &lt;code&gt;SYS&lt;/code&gt; and &lt;code&gt;SYSTEM&lt;/code&gt; keep their original passwords. If you created a &lt;code&gt;dev&lt;/code&gt; user, it still exists with its password unchanged.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;-e ORACLE_PWD&lt;/code&gt; flag only matters when starting with a fresh database (no volume or a new volume name).&lt;/p&gt;

&lt;h2&gt;
  
  
  Best Practice: Create a Dedicated Developer User
&lt;/h2&gt;

&lt;p&gt;The goal of this guide is to move beyond a simple demo and set you up with a professional development environment that mirrors real-world best practices. To do this, we will address two fundamental principles: one for security and one for database health.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Security: The Principle of Least Privilege
&lt;/h3&gt;

&lt;p&gt;First, you should never use the powerful SYSTEM or SYS accounts for everyday application development. Doing so is a security anti-pattern that violates the principle of least privilege. Our first step is to create a dedicated, less-privileged &lt;strong&gt;dev&lt;/strong&gt; user specifically for our application.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Database Health: Separating User Data from System Data
&lt;/h3&gt;

&lt;p&gt;Second, we must address a specific quirk of this "lite" container. To provide the quickest possible startup experience, it intentionally defaults to using its own critical &lt;code&gt;SYSTEM&lt;/code&gt; tablespace for all user data. While this is fine for a temporary "Hello, World!" test, it is a major anti-pattern for building real applications. The SYSTEM tablespace is reserved for the database's core metadata; mixing your app's data with it can risk database stability and would never be allowed in a production environment.&lt;/p&gt;

&lt;p&gt;To build our application on a proper foundation, we will override this default by creating a separate tablespace for our dev user. This ensures our local setup behaves like a real-world database, making our application more robust and portable.&lt;/p&gt;

&lt;p&gt;The following steps show the complete, professional method for creating a new user that adheres to both of these best practices.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;🌩️ These commands should be run as the &lt;code&gt;SYS&lt;/code&gt; user. Connect using your the full &lt;code&gt;docker exec&lt;/code&gt; command.&lt;/p&gt;


&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; oracle-free sqlplus sys/OracleIsAwesome@localhost:1521/freepdb1 as sysdba
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Step 1: Create a Dedicated Tablespace
&lt;/h4&gt;

&lt;p&gt;Create a new tablespace named &lt;code&gt;dev_data&lt;/code&gt; where your user's tables and data will be stored. This is a critical best practice for database health and management.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;TABLESPACE&lt;/span&gt; &lt;span class="n"&gt;dev_data&lt;/span&gt; &lt;span class="n"&gt;DATAFILE&lt;/span&gt; &lt;span class="s1"&gt;'/opt/oracle/oradata/dev_data01.dbf'&lt;/span&gt; &lt;span class="k"&gt;SIZE&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="n"&gt;AUTOEXTEND&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 2: Create the &lt;code&gt;dev&lt;/code&gt; User
&lt;/h4&gt;

&lt;p&gt;Now, create the &lt;strong&gt;dev&lt;/strong&gt; user and assign your new tablespace as its default home right from the start.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;DevPassword123&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;TABLESPACE&lt;/span&gt; &lt;span class="n"&gt;dev_data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🌩️ Note:&lt;/strong&gt; Feel free to replace &lt;code&gt;DevPassword123&lt;/code&gt; with a secure password of your own. Just remember to update it in the shortcut scripts later!&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h4&gt;
  
  
  Step 3: Grant Storage Quota
&lt;/h4&gt;

&lt;p&gt;Allow the user to store data in their new &lt;code&gt;dev_data&lt;/code&gt; tablespace.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt; &lt;span class="n"&gt;QUOTA&lt;/span&gt; &lt;span class="n"&gt;UNLIMITED&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;dev_data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  Step 4: Grant Permissions
&lt;/h4&gt;

&lt;p&gt;Finally, grant the user the rights to connect to the database (&lt;code&gt;CONNECT&lt;/code&gt;) and create objects like tables and views (&lt;code&gt;RESOURCE&lt;/code&gt;).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;GRANT&lt;/span&gt; &lt;span class="k"&gt;CONNECT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RESOURCE&lt;/span&gt; &lt;span class="k"&gt;TO&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F1tt9zyyt0c5g968uagrd.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%2F1tt9zyyt0c5g968uagrd.png" alt="Best Practice" width="800" height="337"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You can now type &lt;code&gt;exit&lt;/code&gt; to leave the SQL*Plus session. &lt;/p&gt;

&lt;p&gt;To connect as your new &lt;code&gt;dev&lt;/code&gt; user, you can use the &lt;code&gt;docker exec&lt;/code&gt; command again. Once connected, run the following quick test to confirm everything works as expected.&lt;/p&gt;

&lt;p&gt;First, connect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; oracle-free sqlplus dev/DevPassword123@localhost:1521/freepdb1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, at the new &lt;code&gt;SQL&amp;gt;&lt;/code&gt; prompt, test your schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Confirm your schema works&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;hello&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;hello&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Welcome to your dev schema'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;hello&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After confirming it works, you can &lt;code&gt;exit&lt;/code&gt; the session.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; To make switching between your &lt;code&gt;dev&lt;/code&gt; user and admin users easier, check out the bonus section at the end of this article for a helpful script you can add to your shell configuration.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why 23ai Might Be the Only Database You Need
&lt;/h2&gt;

&lt;p&gt;Oracle 23ai Free isn't just a traditional relational database. It integrates modern features that allow it to replace other specialized databases in your stack.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better than Postgres?
&lt;/h3&gt;

&lt;p&gt;Oracle 23ai includes all the robust, enterprise-grade relational features you expect (ACID transactions, advanced SQL, PL/SQL). But it also adds unique capabilities like &lt;strong&gt;AI Vector Search&lt;/strong&gt;, allowing you to build powerful semantic search and RAG applications directly within the database.&lt;/p&gt;

&lt;h3&gt;
  
  
  Better than MongoDB?
&lt;/h3&gt;

&lt;p&gt;Oracle's &lt;strong&gt;JSON Relational Duality Views&lt;/strong&gt; are a revolutionary feature. They let you work with data as both structured relational tables and flexible JSON documents—simultaneously. The data is stored once, but can be accessed, modified, and queried from either perspective with full ACID compliance. This eliminates the object-relational mismatch and provides the flexibility of a document store with the power of a relational database.&lt;/p&gt;

&lt;p&gt;This means your application doesn’t need complex mapping layers — your JSON and relational models stay in sync automatically.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Capability&lt;/th&gt;
&lt;th&gt;MongoDB&lt;/th&gt;
&lt;th&gt;Oracle 23ai Free&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Flexible JSON Storage&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transactional JSON&lt;/td&gt;
&lt;td&gt;❌ (Limited)&lt;/td&gt;
&lt;td&gt;✅ (Full ACID)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Combine JSON + Relational&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅ (JSON Duality Views)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Built-in AI Vector Search&lt;/td&gt;
&lt;td&gt;❌&lt;/td&gt;
&lt;td&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

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

&lt;p&gt;Running Oracle Database on a Mac is no longer a chore. With native ARM64 containers, it's fast, efficient, and incredibly easy to get started. You get a world-class, enterprise-grade database with cutting-edge AI and JSON features, all running locally on your machine for free.&lt;/p&gt;

&lt;p&gt;Happy building! ☁️&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus: Faster Connections with Shell Shortcuts
&lt;/h2&gt;

&lt;p&gt;As you work with Oracle, you'll find you sometimes need to perform administrative tasks as a privileged user, but you should use a dedicated dev user for daily work. To make this workflow seamless, you can create a set of shortcuts.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Create a Custom Script File
&lt;/h3&gt;

&lt;p&gt;First, place the shortcut functions into a dedicated file. A common and safe practice is to use a hidden file in your home directory. Create and paste the following into a new file named &lt;code&gt;~/.zsh_oracle_shortcuts&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Oracle DB connection shortcuts&lt;/span&gt;

&lt;span class="c"&gt;# Connect as dev user using sqlplus inside the container&lt;/span&gt;
&lt;span class="k"&gt;function &lt;/span&gt;sql-dev&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; oracle-free sqlplus dev/DevPassword123@localhost:1521/freepdb1
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Connect as dev user using local sqlcl client&lt;/span&gt;
&lt;span class="k"&gt;function &lt;/span&gt;sqlcl-dev&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="c"&gt;# This requires you to have sqlcl installed locally (e.g., via Homebrew)&lt;/span&gt;
    sql dev/DevPassword123@localhost:1521/freepdb1
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Connect as SYSTEM admin using sqlplus inside the container&lt;/span&gt;
&lt;span class="k"&gt;function &lt;/span&gt;sql-system&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; oracle-free sqlplus system/OracleIsAwesome@localhost:1521/freepdb1
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# Connect as SYS superuser using sqlplus inside the container&lt;/span&gt;
&lt;span class="k"&gt;function &lt;/span&gt;sql-sys&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
    docker &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; oracle-free sqlplus sys/OracleIsAwesome@localhost:1521/freepdb1 as sysdba
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  2. Safely Load the Script in Your &lt;code&gt;.zshrc&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Now, add the following lines to the end of your &lt;code&gt;~/.zshrc&lt;/code&gt; file. This code safely checks if your shortcut file exists before trying to load it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Load Oracle DB shortcuts if the file exists&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.zsh_oracle_shortcuts"&lt;/span&gt; &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;source&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.zsh_oracle_shortcuts"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Reload Your Shell
&lt;/h3&gt;

&lt;p&gt;For the changes to take effect, either restart your terminal or run &lt;code&gt;source ~/.zshrc&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Use Your New Shortcuts
&lt;/h3&gt;

&lt;p&gt;You can now connect to your database instantly with these simple commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sql-dev&lt;/code&gt;: Connect as your developer user.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sqlcl-dev&lt;/code&gt;: Connect as your developer user using the local sqlcl tool.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sql-system&lt;/code&gt;: Connect as the &lt;code&gt;SYSTEM&lt;/code&gt; administrator.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;sql-sys&lt;/code&gt;: Connect as the &lt;code&gt;SYS&lt;/code&gt; superuser.&lt;/li&gt;
&lt;/ul&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%2Fm0nll3t7a9pcpi53e2r9.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%2Fm0nll3t7a9pcpi53e2r9.png" alt="sql-dev" width="800" height="338"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Changing the Developer Password
&lt;/h3&gt;

&lt;p&gt;If you need to change the dev user's password in the future, follow these three steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Connect as an Admin:&lt;/strong&gt; Log in as &lt;code&gt;SYSTEM&lt;/code&gt; or SYS using one of your shortcuts (&lt;code&gt;sql-system&lt;/code&gt; or &lt;code&gt;sql-sys&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Alter the User:&lt;/strong&gt; Run the ALTER USER command with your new password:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt; &lt;span class="k"&gt;ALTER&lt;/span&gt; &lt;span class="k"&gt;USER&lt;/span&gt; &lt;span class="n"&gt;dev&lt;/span&gt; &lt;span class="n"&gt;IDENTIFIED&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;MyNewSecret123&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Update Your Shortcuts:&lt;/strong&gt; This is critical. Immediately edit your &lt;code&gt;~/.zsh_oracle_shortcuts&lt;/code&gt; file and update the old password to the new one in both the &lt;code&gt;sql-dev&lt;/code&gt; and &lt;code&gt;sqlcl-dev&lt;/code&gt; functions. If you don't do this, your shortcuts will stop working.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Keeping Your Shortcuts Working After Container Restarts
&lt;/h3&gt;

&lt;p&gt;Your shell shortcuts will continue working seamlessly after recreating the container, as long as:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Container name matches: &lt;code&gt;--name oracle-free&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Volume is attached: &lt;code&gt;-v oracle-data:/opt/oracle/oradata&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Passwords are preserved (automatic when using the same volume)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If all three match, your shortcuts work immediately — no reconfiguration needed.&lt;/p&gt;




&lt;p&gt;Cover Photo by &lt;a href="https://unsplash.com/@boliviainteligente?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;BoliviaInteligente&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/text-_BFJOg1nxaw?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>docker</category>
      <category>macos</category>
      <category>database</category>
    </item>
    <item>
      <title>I Built a Voice-Controlled Plant Sitter in Python with Goose &amp; Gemini CLI</title>
      <dc:creator>Smyekh David-West</dc:creator>
      <pubDate>Thu, 13 Nov 2025 19:02:53 +0000</pubDate>
      <link>https://dev.to/smyekh/i-built-a-voice-controlled-plant-sitter-in-python-with-goose-gemini-cli-5g29</link>
      <guid>https://dev.to/smyekh/i-built-a-voice-controlled-plant-sitter-in-python-with-goose-gemini-cli-5g29</guid>
      <description>&lt;p&gt;Ever killed a houseplant because you forgot to water it? I have. Twice.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Here’s how I built a hands-free plant care assistant, the architectural nightmare I ran into, and how an AI subagent helped me solve it.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  🪴 The Journey
&lt;/h2&gt;

&lt;p&gt;Here’s a look at the journey from a simple seed of an idea to a fully-grown application. Follow along to see how it grew.&lt;/p&gt;

&lt;h3&gt;
  
  
  🌱 Planting the Seed
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Tools for the Job&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌿 The Growth
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Architecture That Almost Broke Me&lt;/li&gt;
&lt;li&gt;The Breakthrough: Embracing Process Isolation&lt;/li&gt;
&lt;li&gt;The Last Mile: Making It Real&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌻 The Harvest
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Partner: My Experience Pair Programming with an LLM&lt;/li&gt;
&lt;li&gt;The Brains: Making the Reminders "Smart"&lt;/li&gt;
&lt;li&gt;Key Lessons Learned&lt;/li&gt;
&lt;li&gt;The Final Product on macOS and Windows&lt;/li&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Happy reading! 🪴&lt;/p&gt;




&lt;p&gt;Mastery requires love. And sometimes, that love leads you into debugging at 2 AM because your ficus deserves better uptime.&lt;/p&gt;

&lt;p&gt;I currently have seven houseplants. That's seven different watering schedules, seven different light preferences, and one sometimes very forgetful owner. Keeping up with their routines can become a real chore. I know because I've lost two from the initial nine to forgetfulness. And yes, they all have names, named after characters from Harry Potter.&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%2Feko6b9vvy1o8anfen88s.JPG" 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%2Feko6b9vvy1o8anfen88s.JPG" alt="Plants"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This simple, real-world problem was the seed for my hackathon project: &lt;strong&gt;Smart Plant Sitter.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The theme of the &lt;a href="https://nokeyboardsallowed.dev/" rel="noopener noreferrer"&gt;CODETV &amp;amp; Goose Hackathon&lt;/a&gt; was to build an app that 'listens, moves, or reacts to anything but your keyboard'. So the vision became to build a voice-only assistant that could manage my plant collection for me. I wanted to be able to say, "Watered Minerva," and have the app remember it for me, no typing required.&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%2Fewykteiad197dpuevedi.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%2Fewykteiad197dpuevedi.png" alt=" "&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Curious to see it in action? A full video demo is waiting for you at the end of the article.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools for the Job
&lt;/h2&gt;

&lt;p&gt;To bring this vision to life, I settled on a modern Python tech stack. From the outset, I wanted to follow &lt;strong&gt;Gall's Law&lt;/strong&gt; as my guiding principle:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"A complex system that works is invariably found to have evolved from a simple system that worked."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That principle guided every decision, from the first prototype to the final packaged app.&lt;/p&gt;

&lt;p&gt;My plan was to start with the simplest possible working version and evolve it. The tools for this simple system were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flet:&lt;/strong&gt; A fantastic framework for building beautiful, multi-platform desktop UIs with Python that uses Flutter under the hood. I needed a UI that would look the same across different platforms.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;FastAPI:&lt;/strong&gt; A high-performance web framework for creating the backend API that would house all the logic. This is part of my standard stack, so it was a no-brainer.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SpeechRecognition &amp;amp; pyttsx3:&lt;/strong&gt; The core libraries for handling voice input and text-to-speech output.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Goose &amp;amp; Gemini CLI:&lt;/strong&gt; My AI development partners in crime for this heist. I used the Gemini CLI within the Goose environment, which gave the AI direct access to my local file system to read, write, and refactor code alongside me.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OpenWeatherMap API:&lt;/strong&gt; To provide real-time weather data for the user's location. This enables the assistant to offer intelligent, context-aware advice.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Local Data Persistence:&lt;/strong&gt; A simple &lt;code&gt;plants.json&lt;/code&gt; file stored in the system's standard application data directory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cross-Platform Design:&lt;/strong&gt; From the beginning, the app was designed to work on macOS, Windows, and Linux. This influenced decisions like using &lt;strong&gt;Flet&lt;/strong&gt; and writing platform-aware code to store the application's data in the correct system directory for each operating system.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Architecture That Almost Broke Me
&lt;/h2&gt;

&lt;p&gt;My initial goal was to create a simple, packageable desktop app. The most common advice for this is to run everything in a single process: the Flet GUI on the main thread and the FastAPI server on a background thread.&lt;/p&gt;

&lt;p&gt;It seemed simple enough. It was not.&lt;/p&gt;

&lt;p&gt;This approach led to a cascade of complex, frustrating, and hard-to-debug issues:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Threading Conflicts:&lt;/strong&gt; The Flet GUI and the voice session were constantly competing for resources, making the app unresponsive.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stateful TTS Engines:&lt;/strong&gt; Text-to-speech libraries like &lt;code&gt;pyttsx3&lt;/code&gt; are stateful and have known threading issues that simple locks can't fix. My app would often speak its first line and then go silent forever.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Blocking I/O:&lt;/strong&gt; The &lt;code&gt;speech_recognition&lt;/code&gt; library blocks the entire thread while it's listening for audio. When run in the same process as the GUI, this would freeze the entire application.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Packaging Complexity:&lt;/strong&gt; Trying to bundle all these conflicting concerns into a single process for packaging was a nightmare.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Breakthrough: Embracing Process Isolation
&lt;/h2&gt;

&lt;p&gt;After two days of late-night debugging with my AI subagent, it was clear: we were fighting a losing battle. The root of all these issues was the single-process model. It was the recurring theme in the troubleshooting session.&lt;/p&gt;

&lt;p&gt;After several iterations I realised that these libraries were never designed to coexist so intimately. The breakthrough came when we stopped forcing these libraries together and instead embraced a classic software design principle: &lt;strong&gt;separation of concerns.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The final, stable architecture is a multi-process client-server model:&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%2Fvjxbiny362w0e2rjqee3.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%2Fvjxbiny362w0e2rjqee3.png" alt="Stable Architecture Diagram"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Flet Frontend:&lt;/strong&gt; It became a pure, lightweight client. Its only job is to display the UI and send HTTP requests to the backend.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The FastAPI Backend:&lt;/strong&gt; Runs in a completely separate, isolated process. It handles all the heavy lifting: the stateful TTS engine, the blocking speech recognition, and all business logic.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This architecture solved everything. The frontend is always responsive, the voice session runs without interference, and the state is cleanly managed in one place. To achieve this, the main &lt;code&gt;frontend.py&lt;/code&gt; script launches a second, identical instance of itself as a subprocess, but passes a special &lt;code&gt;--run-backend&lt;/code&gt; flag. The child process sees this flag and starts the FastAPI server, while the parent process continues on to launch the Flet GUI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# In frontend.py
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;start_backend&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;backend_process&lt;/span&gt;
    &lt;span class="c1"&gt;# Relaunch this same script, but pass a special flag
&lt;/span&gt;    &lt;span class="c1"&gt;# so the new process knows to run the backend server.
&lt;/span&gt;    &lt;span class="n"&gt;command&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;executable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;frontend.py&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--run-backend&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Use DETACHED_PROCESS on Windows to prevent the GUI from freezing
&lt;/span&gt;    &lt;span class="n"&gt;creation_flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;win32&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;creation_flags&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DETACHED_PROCESS&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;CREATE_NO_WINDOW&lt;/span&gt;

    &lt;span class="n"&gt;backend_process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Popen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;creationflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;creation_flags&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Last Mile: Making It Real
&lt;/h2&gt;

&lt;p&gt;With a stable architecture, I turned to packaging. Because a voice-first app must run locally, packaging wasn’t optional—it was survival. But the real world always has one last challenge.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. The macOS Microphone Permission Problem
&lt;/h3&gt;

&lt;p&gt;On macOS, my packaged app was being silently blocked from using the microphone. &lt;code&gt;flet pack&lt;/code&gt; didn't expose the advanced options needed to fix this. The solution was to use PyInstaller directly, generate a &lt;code&gt;.spec&lt;/code&gt; file, and manually add the &lt;code&gt;NSMicrophoneUsageDescription&lt;/code&gt; key to the app's &lt;code&gt;Info.plist&lt;/code&gt;. This gave me the control I needed and was the final key to a working macOS app.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The Windows Reality Check
&lt;/h3&gt;

&lt;p&gt;With the Mac version working, testing on a friend’s Windows laptop reminded me how fragile ‘cross-platform’ can be.&lt;br&gt;
This is where theory meets reality. Working on a borrowed, non-technical machine meant I had to create a perfect, sandboxed development environment that could be wiped clean. It forced me to move beyond my Mac comfort zone and truly understand the OS-level quirks of deployment.&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%2Ff2y19fy7faab5x3x6hgu.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%2Ff2y19fy7faab5x3x6hgu.PNG" alt="Windows Development Sandbox"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The biggest discovery? Windows has two different application data directories (&lt;code&gt;%APPDATA%&lt;/code&gt; and &lt;code&gt;%LOCALAPPDATA%&lt;/code&gt;). My frontend was looking in &lt;code&gt;Local&lt;/code&gt;, while the backend was writing to &lt;code&gt;Roaming&lt;/code&gt;. Finding that bug required hunting through the file system—a reminder that assumptions about "standard" paths don't always hold.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_app_data_dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;win32&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;LOCALAPPDATA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;app_name&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;darwin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# macOS
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Library&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Application Support&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;app_name&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# Linux and other Unix-like
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;home&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;.config&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;app_name&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Partner: My Experience Pair Programming with an LLM
&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%2Fiqry4qt7v33clgpmgb5n.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%2Fiqry4qt7v33clgpmgb5n.png" alt="Goose in the Terminal"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This project was a true collaboration with an AI subagent. Our partnership had a clear division of labor: I handled the Flet frontend and UX, while the AI took on the heavy lifting for the backend architecture and logic—roughly a 60/40 split in its favor.&lt;/p&gt;

&lt;p&gt;My "rule of thumb" when using AI is simple: never let the agent make direct changes. I reviewed every suggestion, understood its implications, and applied the code myself.&lt;/p&gt;

&lt;p&gt;My process is deliberate: I start each session with a preamble to guide the overall direction, and I embed strict instructions like "Answer in chat" or "Explain the proposed changes" in my prompts. This allows me to understand the AI's thought process and verify the implications of every change before I apply the code myself.&lt;br&gt;
This human-in-the-loop process was essential. &lt;/p&gt;

&lt;p&gt;Here are some real examples from my 48-prompt journal:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Architectural Pivot (Prompts 34-37):&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;"Refactor the application to run in a single process... Diagnose the resulting critical runtime failure... Revert the architecture back to the stable multi-process model, but implement it in a package-friendly way using sys.executable..."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;This was the moment we hit the wall and made the crucial decision to pivot.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Advanced Packaging Attempts (Prompts 46-47):&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;"Refactor the application's process management from the subprocess module to Python's multiprocessing module... Following a native development pattern, create a minimal Swift 'Launcher' application in Xcode..."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;These ambitious attempts to solve a cosmetic issue failed, but they taught us the limits of what was worth optimizing.&lt;/p&gt;

&lt;p&gt;Finally, &lt;strong&gt;a practical note:&lt;/strong&gt; this kind of iterative development is compute-intensive. The constant need for the subagent to read multiple files and analyze complex code meant I was glad to have a paid Gemini plan with sufficient credits, as I would have quickly hit the limits of a free tier.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Brains: Making the Reminders "Smart"
&lt;/h2&gt;

&lt;p&gt;To make the assistant genuinely helpful, I built a small "database" of 12 common houseplants. It includes watering intervals based on maturity, sunlight needs, and care tips. This allows the assistant to cross-reference this data with your plant's age and the real-time weather to give specific, actionable advice.&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%2F1qdnbqxlzjaye8hqqryz.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%2F1qdnbqxlzjaye8hqqryz.png" alt="Watering Schedule"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Key Lessons Learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Simple Isn't Always Simple:&lt;/strong&gt; The "obvious" single-process architecture hid enormous complexity. The breakthrough came from embracing the design that fit the tools' constraints.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI Excels at Iteration, Humans Excel at Direction:&lt;/strong&gt; The AI was incredible at refactoring, pattern recognition, and proposing solutions. But the strategic pivots and real-world UX validation still required human insight.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Cross-Platform Is a First-Class Concern:&lt;/strong&gt; You can't bolt on compatibility at the end. Every decision—from file paths to process management—has platform implications.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Testing Beats Theory Every Time:&lt;/strong&gt; No amount of reading can replace actually running your code on the target platform.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;These lessons reshaped how I think about tool choice, architecture, and the creative partnership between humans and AI.&lt;/p&gt;
&lt;h2&gt;
  
  
  The Final Product on macOS and Windows
&lt;/h2&gt;
&lt;h3&gt;
  
  
  macOS
&lt;/h3&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%2Fh80uwm6uy9wonfoysnhr.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%2Fh80uwm6uy9wonfoysnhr.png" alt="Plantsitter UI on macOS"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Windows
&lt;/h3&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%2Fg8c23qqnoc6sdypg6d8c.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%2Fg8c23qqnoc6sdypg6d8c.PNG" alt="Plantsitter UI on Windows"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/twwZ-JAE83g"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;

&lt;p&gt;Building Smart Plant Sitter was a masterclass in the realities of software development. It taught me that architectural elegance is about finding the design that works for your specific constraints. Working with an AI subagent transformed what would have been weeks of work into days, but it also highlighted that AI is a tool that amplifies your skills rather than replacing them.&lt;/p&gt;

&lt;p&gt;This project reminded me that software isn't built - it's grown. And that the best AI tools don’t replace developers; they just make our growth faster, and our bugs more interesting.&lt;/p&gt;

&lt;p&gt;And now, if you'll excuse me, my ZZ Plant is telling me it's thirsty.&lt;/p&gt;




&lt;p&gt;If you’d like to explore the code or replicate the build, you can find the full source code, including my detailed 48-prompt journal. &lt;a href="https://github.com/Smyekh/Smart-Plant-Sitter" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The downloadable application is available for both &lt;a href="https://github.com/Smyekh/Smart-Plant-Sitter/releases/tag/v1.0.0" rel="noopener noreferrer"&gt;macOS and Windows&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>python</category>
      <category>opensource</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Beyond Text: Building an AI-Powered Color Matcher in Oracle FreeSQL</title>
      <dc:creator>Smyekh David-West</dc:creator>
      <pubDate>Tue, 28 Oct 2025 21:00:53 +0000</pubDate>
      <link>https://dev.to/smyekh/beyond-text-building-an-ai-powered-color-matcher-in-oracle-freesql-3549</link>
      <guid>https://dev.to/smyekh/beyond-text-building-an-ai-powered-color-matcher-in-oracle-freesql-3549</guid>
      <description>&lt;h2&gt;
  
  
  ☁️ Pre-Flight Checklist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This is a connection flight&lt;/strong&gt;. Before we taxi down the runway, here’s your flight plan. Keep this handy to navigate your flight path. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Welcome aboard the cloud!&lt;/strong&gt; ☁️&lt;/p&gt;

&lt;h3&gt;
  
  
  🌥️ Takeoff
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Core Idea: Colors are Vectors&lt;/li&gt;
&lt;li&gt;Step 1: Create Your Color Palette&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⛅️ Cruising Altitude
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Step 2: The Next Level of PL/SQL - A Reusable Function&lt;/li&gt;
&lt;li&gt;A Quick Note: Where is the Embedding Model?&lt;/li&gt;
&lt;li&gt;Step 3: The Ultimate Color Search&lt;/li&gt;
&lt;li&gt;Step 4: Exploring Different Distance Metrics&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌤️ Landing &amp;amp; Taxi
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Conclusion: From Raw Data to a Practical Tool&lt;/li&gt;
&lt;li&gt;Next Steps &amp;amp; Going Further&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enjoy your flight! ☁️&lt;/p&gt;




&lt;p&gt;In the &lt;a href="https://dev.to/smyekh/your-first-ai-powered-search-on-oracle-cloud-a-beginners-guide-to-vectors-6am"&gt;first part of this series&lt;/a&gt;, we introduced the exciting world of AI Vector Search by finding similar cars using Oracle's new &lt;code&gt;VECTOR&lt;/code&gt; data type. We learned that vectors act as "GPS coordinates for meaning," allowing us to find conceptually similar items.&lt;/p&gt;

&lt;p&gt;But vector search isn't just for text and abstract concepts. It's a powerful tool for searching &lt;em&gt;any&lt;/em&gt; data that you can represent with numbers.&lt;/p&gt;

&lt;p&gt;In this tutorial, we'll build a genuinely useful tool for web developers and designers: a &lt;strong&gt;Color Matcher&lt;/strong&gt;. We'll use Oracle AI Vector Search to find the closest standard CSS color name to any given hex code. And just like last time, we'll do it all for free. We're using the Oracle FreeSQL sandbox because it gives us instant, browser-based access to a full-featured, modern Oracle 23ai Database without any installation or setup required.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Idea: Colors are Vectors
&lt;/h2&gt;

&lt;p&gt;The magic of this project is that colors are already vectors in disguise. Every color on the screen can be represented by the amount of Red, Green, and Blue it contains. This RGB value is a natural 3-dimensional vector.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A pure, vibrant red is &lt;code&gt;rgb(255, 0, 0)&lt;/code&gt;, which becomes the vector &lt;code&gt;[255, 0, 0]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A dark, moody blue like &lt;code&gt;rgb(25, 25, 112)&lt;/code&gt; becomes &lt;code&gt;[25, 25, 112]&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;White is &lt;code&gt;[255, 255, 255]&lt;/code&gt; and Black is &lt;code&gt;[0, 0, 0]&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;code&gt;VECTOR_DISTANCE&lt;/code&gt; between two of these color vectors measures their perceptual similarity. A small distance means the colors look very similar to the human eye. This is the simple but powerful principle we'll use to build our tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create Your Color Palette
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Create the Table
&lt;/h3&gt;

&lt;p&gt;First, we need a dataset to search against. We will create a table containing all the standard named colors used in CSS.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;--Create the table&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;css_colors&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;hex&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="n"&gt;color_vector&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run the statement after pasting use crtl + Enter or ⌘ + Enter&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%2F2gviidezse2x57o737b9.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%2F2gviidezse2x57o737b9.png" alt="Create Table Image" width="800" height="498"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The new &lt;code&gt;css_colors&lt;/code&gt; table will now appear in the navigator pane on the left. A great feature of FreeSQL is that it automatically persists your work, so this table will be available in future sessions.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Populate the Table
&lt;/h3&gt;

&lt;p&gt;I've placed the complete SQL script, which inserts all 140+ colors, into a GitHub Gist. &lt;/p&gt;

&lt;p&gt;The complete SQL script to insert all 140+ colors is available in this &lt;a href="https://gist.github.com/Smyekh/6bab4ef25a9d711b7de7500fe3cf0819" rel="noopener noreferrer"&gt;GitHub Gist&lt;/a&gt;. &lt;br&gt;
Please copy the entire script from the Gist and run it once in a new FreeSQL worksheet to populate the table.&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%2Fmdtrtbz34tvnj2yj6o1h.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%2Fmdtrtbz34tvnj2yj6o1h.png" alt="GITHUB GIST SCRIPT Image" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;After running the script, the &lt;code&gt;css_colors&lt;/code&gt; table will be created and fully populated. You can confirm the data is there with a quick query:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hex&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;css_colors&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="k"&gt;IN&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Red'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Green'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Blue'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fc09zyz5xvkwb8622xjuc.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%2Fc09zyz5xvkwb8622xjuc.png" alt="Population Confirmation Image" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The Next Level of PL/SQL - A Reusable Function
&lt;/h2&gt;

&lt;p&gt;Here is where we move beyond simple queries and begin to leverage the &lt;strong&gt;true power of FreeSQL as a development platform&lt;/strong&gt;. Instead of performing the hex-to-vector conversion in an external application, we can build a smart, reusable function directly in the database. This is far more efficient and demonstrates a core capability of a professional database environment. This is the perfect opportunity to level up our PL/SQL skills by creating a &lt;strong&gt;reusable function&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Copy and paste this code into your FreeSQL worksheet and run it to create the function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;OR&lt;/span&gt; &lt;span class="k"&gt;REPLACE&lt;/span&gt; &lt;span class="k"&gt;FUNCTION&lt;/span&gt; &lt;span class="n"&gt;hex_to_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_hex_string&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt;
  &lt;span class="n"&gt;v_hex&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;v_r&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;v_g&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;v_b&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="c1"&gt;-- Remove the '#' prefix if it exists&lt;/span&gt;
  &lt;span class="n"&gt;v_hex&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;LTRIM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p_hex_string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'#'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;-- Check for valid length&lt;/span&gt;
  &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;LENGTH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_hex&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
    &lt;span class="n"&gt;RAISE_APPLICATION_ERROR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20001&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Invalid hex code: Must be 6 characters long.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;-- Convert hex pairs to decimal numbers, with error handling&lt;/span&gt;
  &lt;span class="k"&gt;BEGIN&lt;/span&gt;
    &lt;span class="n"&gt;v_r&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TO_NUMBER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SUBSTR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'XX'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;v_g&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TO_NUMBER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SUBSTR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'XX'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="n"&gt;v_b&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TO_NUMBER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SUBSTR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_hex&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="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'XX'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;EXCEPTION&lt;/span&gt;
    &lt;span class="k"&gt;WHEN&lt;/span&gt; &lt;span class="n"&gt;OTHERS&lt;/span&gt; &lt;span class="k"&gt;THEN&lt;/span&gt;
      &lt;span class="n"&gt;RAISE_APPLICATION_ERROR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;20002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Invalid hex code: Contains non-hexadecimal characters.'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;-- Return the final result using the string constructor for reliability&lt;/span&gt;
   &lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'['&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;TO_CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;TO_CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;','&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;TO_CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="s1"&gt;']'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  How it Works:
&lt;/h3&gt;

&lt;h4&gt;
  
  
  Lines 1-6: The Setup Area
&lt;/h4&gt;

&lt;p&gt;This section defines our tool. It says we're creating a function named &lt;code&gt;hex_to_vector&lt;/code&gt; that accepts one piece of text (the hex code) and promises to return a VECTOR. We also set up a few empty "boxes" (variables) to hold our values as we work.&lt;/p&gt;

&lt;h4&gt;
  
  
  Line 8: Cleaning the Input
&lt;/h4&gt;

&lt;p&gt;The &lt;code&gt;LTRIM&lt;/code&gt; function tidies up the input by trimming the &lt;code&gt;#&lt;/code&gt; symbol from the left side. This lets us work with just the six important characters of the color code.&lt;/p&gt;

&lt;h4&gt;
  
  
  Lines 11-13: First Safety Check.
&lt;/h4&gt;

&lt;p&gt;A valid hex color code must contain exactly six characters. This &lt;code&gt;IF&lt;/code&gt; statement checks the length. If it's not six, the function stops immediately and uses &lt;code&gt;RAISE_APPLICATION_ERROR&lt;/code&gt; to show a helpful, custom error message instead of just crashing.&lt;/p&gt;

&lt;h4&gt;
  
  
  Lines 16-23: The "Try-Catch" Safety Net.
&lt;/h4&gt;

&lt;p&gt;This &lt;code&gt;BEGIN...EXCEPTION...END&lt;/code&gt; block is the most important safety feature.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Inside &lt;code&gt;BEGIN&lt;/code&gt;, we try the riskiest part: converting the text characters into the numbers for our vector. The code grabs two characters at a time (e.g., 9B) and converts them from hexadecimal into a regular number (e.g., 155).&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;EXCEPTION&lt;/code&gt; block is our safety net. If a user enters something invalid like &lt;code&gt;#FFG000&lt;/code&gt;, the TO_NUMBER function will fail on the character 'G'. Instead of crashing, the code immediately jumps to the EXCEPTION block, which catches the error and displays our second helpful message.&lt;/li&gt;
&lt;/ul&gt;

&lt;h4&gt;
  
  
  Line 26: Assembling the Final Vector.
&lt;/h4&gt;

&lt;p&gt;If the input has passed all our safety checks, this final line builds the result. It converts the three numbers for red, green, and blue into text and carefully assembles them into a string with brackets and commas, like '[155,89,182]'. This string is then passed to the VECTOR function, which creates the final vector object. We use this string-building method because it's a very reliable way to create a vector that avoids potential database bugs.&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%2Fxbnt8yh254jcpeji63p5.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%2Fxbnt8yh254jcpeji63p5.png" alt="Function" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Once you run the script, the function is created. You can see it listed under the Functions tab in the navigator pane on the left. A great feature of FreeSQL is that it automatically saves your functions and tables, so you can come back and use them in future sessions without having to create them again.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  A Quick Note: Where is the Embedding Model?
&lt;/h2&gt;

&lt;p&gt;If you're familiar with AI search, you might be wondering: where is the "embedding model"? It's a great question, and the answer reveals the different ways vectors can be created.&lt;/p&gt;

&lt;p&gt;Whether you need an embedding model depends entirely on your source data. In practice, there are three main approaches to generating vectors:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Natural Vectors (The Color Example)
&lt;/h3&gt;

&lt;p&gt;Our color data (&lt;code&gt;#0000FF&lt;/code&gt;) is a direct, universally defined representation of a vector (&lt;code&gt;[0, 0, 255]&lt;/code&gt;). The data is already a vector. Our &lt;code&gt;hex_to_vector&lt;/code&gt; function isn't an AI model; it's just a decoder that translates one format to another. No model is needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Feature-Engineered Vectors (The Car Example)
&lt;/h3&gt;

&lt;p&gt;In the first article, we used a vector like &lt;code&gt;[9.5, 8]&lt;/code&gt; to represent a car's &lt;code&gt;[Performance, FuelConsumption]&lt;/code&gt;. This is a "middle ground." If your source data is structured (e.g., a table with &lt;code&gt;horsepower&lt;/code&gt; and &lt;code&gt;mpg&lt;/code&gt; columns), you can manually create a vector by defining a formula. For example, &lt;code&gt;Performance = horsepower / 50&lt;/code&gt;. This is a classic data science technique called &lt;strong&gt;feature engineering&lt;/strong&gt;. No AI model is needed because a human is defining the logic.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. AI-Generated Vectors (Text &amp;amp; Image Search)
&lt;/h3&gt;

&lt;p&gt;If your source data is unstructured (e.g., a text description like "a sleek sports car with a roaring V8 engine"), you have no numbers to start with. This is where you must use an embedding model. The AI model reads the text and creates a vector that captures its semantic meaning.&lt;/p&gt;

&lt;p&gt;Here’s a summary:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Data Source&lt;/th&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Model Needed?&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Natural Vector&lt;/td&gt;
&lt;td&gt;Direct Decoding&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Colors (&lt;code&gt;#FF0000&lt;/code&gt; -&amp;gt; &lt;code&gt;[255,0,0]&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Structured Data&lt;/td&gt;
&lt;td&gt;Feature Engineering&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Cars (using &lt;code&gt;horsepower&lt;/code&gt;, &lt;code&gt;mpg&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unstructured Data&lt;/td&gt;
&lt;td&gt;AI Embedding&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Text ("a sleek sports car...")&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Understanding this distinction is key to knowing which tools to use for your own vector search projects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The Ultimate Color Search
&lt;/h2&gt;

&lt;p&gt;Now that we have our data table and our helper function, we can perform the search. This single, elegant query combines everything we've built.&lt;/p&gt;

&lt;p&gt;Let's find the closest named colors to a custom purple, &lt;code&gt;#9B59B6&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Find the 5 closest named colors to a given hex code&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hex_to_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#9B59B6'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;color_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EUCLIDEAN&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;css_colors&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run this, you'll get a result like:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Swatch&lt;/th&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;th&gt;Hex&lt;/th&gt;
&lt;th&gt;Distance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2FBA55D3%2FBA55D3.png" alt="MediumOrchid swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;MediumOrchid&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#BA55D3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;42.64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2F9370DB%2F9370DB.png" alt="MediumPurple swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;MediumPurple&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#9370DB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;44.29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2F9932CC%2F9932CC.png" alt="DarkOrchid swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;DarkOrchid&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#9932CC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;44.82&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2F6A5ACD%2F6A5ACD.png" alt="SlateBlue swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;SlateBlue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#6A5ACD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;54.14&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2F8A2BE2%2F8A2BE2.png" alt="BlueViolet swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;BlueViolet&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#8A2BE2&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;65.89&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It works perfectly! The database calculated the "visual distance" between our custom purple and all 140+ colors in the table and returned the 5 closest matches.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;🌩️ A Quick Clarification:&lt;/strong&gt; The Swatch column is purely for visual presentation in this article. Our SQL query retrieves the raw data—the color name, hex code, and distance—from the database. The database itself simply stores and returns the data, not the visual swatch.&lt;/p&gt;
&lt;/blockquote&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%2Ffjvdsa3o3b7f0o9v8qnl.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%2Ffjvdsa3o3b7f0o9v8qnl.png" alt="EUCLIDEAN DISTANCE" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Exploring Different Distance Metrics
&lt;/h2&gt;

&lt;p&gt;So far, we've used &lt;code&gt;EUCLIDEAN&lt;/code&gt; distance to find the most visually similar colors. But one of the most powerful features of Oracle AI Vector Search is its support for multiple &lt;strong&gt;distance algorithms&lt;/strong&gt;. Each one measures "similarity" in a slightly different way, which can lead to different results.&lt;/p&gt;

&lt;p&gt;Let's turn our query into a more powerful educational tool by making the distance metric itself interactive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Find the 5 closest colors using a user-provided hex code AND distance metric&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hex_to_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'&amp;amp;your_hex_code'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; 
  &lt;span class="n"&gt;color_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;distance_metric&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;css_colors&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, when you run this, FreeSQL will prompt you for both the hex code and the distance metric (e.g., &lt;code&gt;EUCLIDEAN&lt;/code&gt;, &lt;code&gt;COSINE&lt;/code&gt;, &lt;code&gt;DOT&lt;/code&gt;, &lt;code&gt;HAMMING&lt;/code&gt;, or &lt;code&gt;MANHATTAN&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%2F2fsd6cee2kr4w67ai80j.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%2F2fsd6cee2kr4w67ai80j.png" alt="HEX_CODE PROMPT" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A Critical Warning: The Default Metric and the 'Black' Sheep
&lt;/h3&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%2Fzbne6q21m2u05ce50rqc.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%2Fzbne6q21m2u05ce50rqc.png" alt="COSINE PROMPT" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What happens if you don't specify a metric in the prompt? It's crucial to know that &lt;code&gt;COSINE&lt;/code&gt; is the default. If you try to run the query with COSINE (or leave the &lt;code&gt;&amp;amp;distance_metric&lt;/code&gt; prompt blank), you'll hit an error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ORA-03087: unable to convert BINARY_FLOAT or BINARY_DOUBLE NaN (Not a Number) value to NUMBER
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fqj7wy4ljodbp4w5o0cks.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%2Fqj7wy4ljodbp4w5o0cks.png" alt="COSINE ERROR" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This happens because your &lt;code&gt;SELECT&lt;/code&gt; query must calculate the distance between your input color and &lt;strong&gt;every other color&lt;/strong&gt; in the &lt;code&gt;css_colors&lt;/code&gt; table to find the top 5 closest matches. When the query's process reaches the row for "Black", it attempts to calculate the distance between your color and the vector &lt;code&gt;[0, 0, 0]&lt;/code&gt;. Since the "Black" vector has a length of 0, and the Cosine formula involves dividing by the vector's length, this specific comparison fails with a division-by-zero error, stopping the entire query. This is a fantastic practical lesson: always know your defaults and how they interact with your &lt;em&gt;entire dataset&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparing The Results
&lt;/h3&gt;

&lt;p&gt;Let's see how the top 5 closest colors to our custom purple (&lt;code&gt;#9B59B6&lt;/code&gt;) change when we use different metrics. The original &lt;code&gt;EUCLIDEAN&lt;/code&gt; search gave us a list of purples and blues. But other metrics see the world differently.&lt;/p&gt;

&lt;h4&gt;
  
  
  EUCLIDEAN Distance
&lt;/h4&gt;

&lt;p&gt;This is the metric we used in Step 3, and it's the most intuitive way to measure distance. &lt;code&gt;EUCLIDEAN&lt;/code&gt; distance calculates the straight-line, "as the crow flies" path between the two color vectors in 3D space. It's calculated using the Pythagorean formula: &lt;code&gt;sqrt((R1-R2)^2 + (G1-G2)^2 + (B1-B2)^2)&lt;/code&gt;. As the results from the previous step showed, this metric is excellent for finding colors that are perceptually very similar.&lt;/p&gt;

&lt;h4&gt;
  
  
  DOT Product Results
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hex_to_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#9B59B6'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;color_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DOT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fplvl75gofvf8p8acvhs9.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%2Fplvl75gofvf8p8acvhs9.png" alt="DOT PROMPT" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;DOT&lt;/code&gt; product is a similarity score, not a true distance. It's calculated by multiplying the corresponding R, G, and B values of the input vector and the vector it's being compared to (which happens for every vector in the table): &lt;code&gt;(R1*R2) + (G1*G2) + (B1*B2)&lt;/code&gt;. A larger value means more similarity. Oracle returns the negative of the dot product so that smaller results are still "closer." This metric favors colors that are bright in the same channels as our input color. This is because a high value in one vector (e.g., a high Red value in your input) only contributes significantly to the total score if it's multiplied by a correspondingly high value in the other vector. The final score is the sum of these products, so it's maximized when brightness in the R, G, and B channels align. Notice how it returns bright, almost white colors, as they have high R, G, and B values that contribute to a large dot product.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Swatch&lt;/th&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;th&gt;Hex&lt;/th&gt;
&lt;th&gt;Distance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2FFFFFFF%2FFFFFFF.png" alt="White swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;White&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#FFFFFF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-108630&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2FFFFAFA%2FFFFAFA.png" alt="Snow swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;Snow&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#FFFAFA&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-107275&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2FF8F8FF%2FF8F8FF.png" alt="GhostWhite swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;GhostWhite&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#F8F8FF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-106922&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2FF0FFFF%2FF0FFFF.png" alt="Azure swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;Azure&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#F0FFFF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-106305&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2FF5FFFA%2FF5FFFA.png" alt="MintCream swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;MintCream&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#F5FFFA&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;-106170&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fd2xhiw7as9tut3fjxqvg.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%2Fd2xhiw7as9tut3fjxqvg.png" alt="DOT RESULT" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  HAMMING Distance Results
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hex_to_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#9B59B6'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;color_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HAMMING&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fvp0o5novxxzm6pevy36c.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%2Fvp0o5novxxzm6pevy36c.png" alt="HAMMING PROMPT" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HAMMING&lt;/code&gt; distance simply counts how many components (R, G, or B) are different between two vectors. For &lt;code&gt;[R1, G1, B1]&lt;/code&gt; and &lt;code&gt;[R2, G2, B2]&lt;/code&gt;, it checks if &lt;code&gt;R1 != R2&lt;/code&gt;, &lt;code&gt;G1 != G2&lt;/code&gt;, and &lt;code&gt;B1 != B2&lt;/code&gt; and sums the true results. The maximum possible distance is 3. This metric is not very nuanced for colors, as it only cares if the values are an exact match or not, which is rare. This is why you see a tie between several colors that all differ from our input purple in all three (R, G, and B) components.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Swatch&lt;/th&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;th&gt;Hex&lt;/th&gt;
&lt;th&gt;Distance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2FF0F8FF%2FF0F8FF.png" alt="AliceBlue swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;AliceBlue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#F0F8FF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2FFAEBD7%2FFAEBD7.png" alt="AntiqueWhite swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;AntiqueWhite&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#FAEBD7&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2F00FFFF%2F00FFFF.png" alt="Aqua swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;Aqua&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#00FFFF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2F7FFFD4%2F7FFFD4.png" alt="Aquamarine swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;Aquamarine&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#7FFFD4&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2FF0FFFF%2FF0FFFF.png" alt="Azure swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;Azure&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#F0FFFF&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Flvz0rmbqenzef4ohdazn.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%2Flvz0rmbqenzef4ohdazn.png" alt="HAMMING RESULT" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  MANHATTAN Distance Results
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hex_to_vector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'#9B59B6'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;color_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MANHATTAN&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;distance&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fairxmcvrpzryw18tb5cp.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%2Fairxmcvrpzryw18tb5cp.png" alt="MANHATTAN PROMPT" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;MANHATTAN&lt;/code&gt; distance (or "city block" distance) is calculated by summing the absolute differences of each component: &lt;code&gt;|R1 - R2| + |G1 - G2| + |B1 - B2|&lt;/code&gt;. It's like traveling on a grid, one axis at a time. The results are very similar to EUCLIDEAN distance, as both measure perceptual difference, but MANHATTAN gives slightly more weight to the sum of individual channel differences than the direct "as the crow flies" distance.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Swatch&lt;/th&gt;
&lt;th&gt;Color&lt;/th&gt;
&lt;th&gt;Hex&lt;/th&gt;
&lt;th&gt;Distance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2F9932CC%2F9932CC.png" alt="DarkOrchid swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;DarkOrchid&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#9932CC&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;63&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2FBA55D3%2FBA55D3.png" alt="MediumOrchid swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;MediumOrchid&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#BA55D3&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;64&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2F9370DB%2F9370DB.png" alt="MediumPurple swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;MediumPurple&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#9370DB&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;68&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2F6A5ACD%2F6A5ACD.png" alt="SlateBlue swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;SlateBlue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#6A5ACD&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;73&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fplacehold.co%2F14x14%2F7B68EE%2F7B68EE.png" alt="MediumSlateBlue swatch" width="14" height="14"&gt;&lt;/td&gt;
&lt;td&gt;MediumSlateBlue&lt;/td&gt;
&lt;td&gt;&lt;code&gt;#7B68EE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;103&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Foun7g1rb7z1xyxo0whuh.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%2Foun7g1rb7z1xyxo0whuh.png" alt="MANHATTAN RESULT" width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;As you can see, the "best" match depends entirely on your definition of similarity. For finding visually similar colors, EUCLIDEAN or MANHATTAN are clearly the best choices. For other tasks, other metrics might be superior. Choosing the right one is a critical part of designing a successful vector search application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion: From Raw Data to a Practical Tool
&lt;/h2&gt;

&lt;p&gt;Congratulations! You've built a practical color-matching tool and experienced the power of a professional database environment firsthand.&lt;/p&gt;

&lt;p&gt;In this tutorial, you have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Seen how vector search can be applied to non-text data like colors.&lt;/li&gt;
&lt;li&gt;Leveled up your PL/SQL skills by creating a powerful, reusable function.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Experienced how FreeSQL provides a zero-install sandbox to build and test real, data-centric application logic.&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This reinforces the power of keeping your logic close to your data. By creating a function directly in the database, we built a fast, efficient, and elegant solution.&lt;/p&gt;

&lt;p&gt;Happy building! ☁️&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps &amp;amp; Going Further
&lt;/h2&gt;

&lt;p&gt;The tool you've built is complete, but the concepts can go even further. Here are a few ideas to keep in mind for future projects:&lt;/p&gt;

&lt;h3&gt;
  
  
  Indexing for Performance
&lt;/h3&gt;

&lt;p&gt;Our table of 140 colors is tiny for an Oracle database. But if you were building a search engine for millions of items, a sequential scan would be too slow. For this, you would add a vector index.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Example of creating a vector index&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;color_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;css_colors&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color_vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ORGANIZATION&lt;/span&gt; &lt;span class="n"&gt;INMEMORY&lt;/span&gt; &lt;span class="n"&gt;NEIGHBOR&lt;/span&gt; &lt;span class="n"&gt;GRAPH&lt;/span&gt;
&lt;span class="n"&gt;DISTANCE&lt;/span&gt; &lt;span class="n"&gt;COSINE&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command creates an Approximate Nearest Neighbor (ANN) index, which allows the database to find the "closest" vectors almost instantly without having to compare your query with every single row. This is the critical step for taking a vector search prototype to a production-level application.&lt;/p&gt;

&lt;h3&gt;
  
  
  Exploring Other Use Cases
&lt;/h3&gt;

&lt;p&gt;This vector technique is powerful for internal resource management. Imagine a project manager needs to assign a critical task: "Build a data-driven backend prototype and present it to stakeholders." The required skills could be quantified as a vector, for example: &lt;code&gt;[Python, SQL, PublicSpeaking, Design]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The ideal profile for this specific task might be &lt;code&gt;[9, 8, 7, 1]&lt;/code&gt;, representing a need for strong Python/SQL, good presentation skills, and no design experience.&lt;/p&gt;

&lt;p&gt;Instead of manually searching through employee profiles or relying on memory, the manager could run a query to find the best fit instantly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Find the best available employee for a specific task profile&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
  &lt;span class="n"&gt;employee_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;current_project_load&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;employees&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;availability&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'Available'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;skill_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[9, 8, 7, 1]'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This query would instantly surface the top 3 available employees whose skills most closely match the task's requirements. It's a data-driven approach that enables fast, effective, and unbiased task allocation within an organization.&lt;/p&gt;

&lt;p&gt;Keep exploring what you can build! ☁️&lt;/p&gt;




&lt;p&gt;Cover Photo by &lt;a href="https://unsplash.com/@boliviainteligente?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;BoliviaInteligente&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/text-_BFJOg1nxaw?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>ai</category>
      <category>sql</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Your First AI-Powered Search on Oracle Cloud: A Beginner's Guide to Vectors</title>
      <dc:creator>Smyekh David-West</dc:creator>
      <pubDate>Thu, 23 Oct 2025 23:54:22 +0000</pubDate>
      <link>https://dev.to/smyekh/your-first-ai-powered-search-on-oracle-cloud-a-beginners-guide-to-vectors-6am</link>
      <guid>https://dev.to/smyekh/your-first-ai-powered-search-on-oracle-cloud-a-beginners-guide-to-vectors-6am</guid>
      <description>&lt;h2&gt;
  
  
  ☁️ Pre-Flight Checklist
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;This is a connection flight&lt;/strong&gt;. Before we taxi down the runway, here’s your flight plan. Keep this handy to navigate your flight path. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Welcome aboard the cloud!&lt;/strong&gt; ☁️&lt;/p&gt;

&lt;h3&gt;
  
  
  🌥️ Takeoff
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The Core Idea: What is a Vector, Anyway?&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Your Free AI Sandbox: No Install Required&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⛅️ Cruising Altitude
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Step 1: Create Your First Vector-Enabled Table&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Step 2: Insert Your Car Profiles&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Step 3: Find Similar Cars with SQL&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;From SQL to Application Logic with PL/SQL&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌤️ Landing &amp;amp; Taxi
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Next Steps: Performance and Other Metrics&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Conclusion: Your Journey into AI on OCI&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enjoy your flight! ☁️&lt;/p&gt;




&lt;p&gt;Let's be honest, as developers, we're used to precise, literal searches. If you query your database for &lt;code&gt;status = 'completed'&lt;/code&gt;, you get records where the status is exactly 'completed'. But what if you wanted to find things that are &lt;em&gt;conceptually similar&lt;/em&gt;? What if you wanted to find a "quick automobile" when your data only contains "fast car"?&lt;/p&gt;

&lt;p&gt;That's where the world of AI and vector search comes in, and it's not as scary or complicated as it sounds. In fact, if you know basic SQL, you're already halfway there.&lt;/p&gt;

&lt;p&gt;Today, we'll break down the fundamentals of vector search and show you how to run your very first similarity search using nothing but your browser and Oracle's completely free SQL sandbox, a service that's part of the Oracle Cloud ecosystem.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Idea: What is a Vector, Anyway?
&lt;/h2&gt;

&lt;p&gt;Forget the complex math for a second. Think of a vector as &lt;strong&gt;GPS coordinates for meaning&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;An AI model, called an "embedding model", reads a piece of data and converts it into a list of numbers. For our car example, imagine a vector where the first number is performance (higher is better) and the second is fuel consumption (lower is better).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A "sleek, powerful sports car" might get a vector like &lt;code&gt;[9.5, 8]&lt;/code&gt; (High performance, high fuel use).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;An "efficient, reliable hybrid" might get a vector like &lt;code&gt;[6, 3]&lt;/code&gt; (Medium performance, low fuel use).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The goal of vector search is to find the "nearest neighbors" to a query vector by calculating the distance between these coordinates.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your Free AI Sandbox: No Install Required
&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%2Fsh818o8bzl9dn2r9h6uj.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%2Fsh818o8bzl9dn2r9h6uj.png" alt="ORACLE FREESQL"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; Go to &lt;strong&gt;&lt;a href="https://livesql.oracle.com/" rel="noopener noreferrer"&gt;https://livesql.oracle.com/&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt; Sign in with a free Oracle account (or create one).&lt;/li&gt;
&lt;li&gt; Once you login, you should see the page layout below&lt;/li&gt;
&lt;/ol&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%2Ftoh9cp804zd6ji1wmmq3.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%2Ftoh9cp804zd6ji1wmmq3.png" alt="Free SQL DASH"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That's it! You have a full-featured Oracle Database ready for your commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Create Your First Vector-Enabled Table
&lt;/h2&gt;

&lt;p&gt;In Oracle Database 23ai, storing these "meaning coordinates" is incredibly simple with the new &lt;code&gt;VECTOR&lt;/code&gt; data type.&lt;/p&gt;

&lt;p&gt;Let's create a table to store our car profiles. Copy and paste this into your SQL worksheet:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;car_profiles&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
   &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
   &lt;span class="n"&gt;car_description&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
   &lt;span class="n"&gt;performance_consumption_vector&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run the statement after pasting use &lt;strong&gt;crtl + Enter&lt;/strong&gt; or &lt;strong&gt;⌘ + Enter&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%2Fz08d3o1mz8p9rnlchnuc.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%2Fz08d3o1mz8p9rnlchnuc.png" alt="CREATE TABLE"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once you create a table, FreeSQL automatically saves it for your next session. &lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Insert Your Car Profiles
&lt;/h2&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;A Quick Note:&lt;/strong&gt; In a real application, an AI model would generate these vectors. For the purpose of this demonstration, we'll create simple, 2-dimensional vectors ourselves to focus on learning the powerful database-side of vector search.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Here are the profiles we'll be working with, where the vector represents &lt;code&gt;[Performance, Fuel Consumption]&lt;/code&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;ID&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;th&gt;Vector&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;"Perfect high-performance hybrid"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[10,2]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Ideal car profile&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;"Sporty fuel-efficient coupe"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[9,2.5]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Slightly less powerful but efficient&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;"Old gas-guzzling truck"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[4,10]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Poor fuel economy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;"Balanced midrange sedan"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[7,5]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Decent balance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;"Economy compact car"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[6,3]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Modest power, great fuel economy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;"Luxury performance SUV"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[9.5,6]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Powerful but thirsty&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;"Underpowered city car"&lt;/td&gt;
&lt;td&gt;&lt;code&gt;[3,4]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Low performance, moderate efficiency&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Let's insert this data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt; &lt;span class="k"&gt;ALL&lt;/span&gt;
  &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;car_profiles&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Perfect high-performance hybrid'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[10, 2]'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;car_profiles&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Sporty fuel-efficient coupe'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[9, 2.5]'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;car_profiles&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Old gas-guzzling truck'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[4, 10]'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;car_profiles&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Balanced midrange sedan'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[7, 5]'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;car_profiles&lt;/span&gt; &lt;span class="k"&gt;VALUES&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="s1"&gt;'Economy compact car'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[6, 3]'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;car_profiles&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Luxury performance SUV'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[9.5, 6]'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;car_profiles&lt;/span&gt; &lt;span class="k"&gt;VALUES&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Underpowered city car'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[3, 4]'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;DUAL&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2F9rz6ov1d1630w7il0i76.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%2F9rz6ov1d1630w7il0i76.png" alt="Tableau 1"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- To confirm that the data has been inserted into the table&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;car_profiles&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fuzvske17oiv28aa4ymg5.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%2Fuzvske17oiv28aa4ymg5.png" alt="Tableau"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;In Oracle SQL:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;A single-row &lt;code&gt;INSERT INTO ... VALUES (...)&lt;/code&gt; statement doesn’t need a source because it’s one direct action.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;A multi-row insert ('INSERT ALL') is conceptually a query-based insert.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When you want to insert multiple literal rows (not coming from another table), you don’t have a real source. Oracle solves that by letting you use the DUAL table, which is a built-in one-row, one-column table.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;SELECT * FROM DUAL&lt;/code&gt; acts as a placeholder that makes Oracle happy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Find Similar Cars with SQL
&lt;/h2&gt;

&lt;p&gt;The key to vector search is the &lt;code&gt;VECTOR_DISTANCE&lt;/code&gt; function. It's like a ruler that measures the distance between our query vector and all the vectors in our table. Let's use it to rank all cars in our database by how similar they are to our "Ideal Car Profile" (&lt;code&gt;[10, 2]&lt;/code&gt;). A lower distance means a better match.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;car_description&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[10, 2]'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;performance_consumption_vector&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;similarity_score&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;car_profiles&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;similarity_score&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;ROUND()&lt;/code&gt;function makes numbers easier to read by limiting how many decimal places are shown. &lt;/p&gt;

&lt;p&gt;&lt;code&gt;ASC&lt;/code&gt; stands for ASCENDING order — it’s part of the ORDER BY clause in SQL.&lt;/p&gt;
&lt;/blockquote&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%2F1mb27r1rxok3468v45n1.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%2F1mb27r1rxok3468v45n1.png" alt="TABLEAU 3"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Understanding the similarity scores
&lt;/h3&gt;

&lt;p&gt;In the previous query, we compared every car’s vector against our “ideal” car vector &lt;code&gt;[10, 2]&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;VECTOR_DISTANCE&lt;/code&gt; function measures how far apart two vectors are, just like measuring the straight-line distance between two points on a map.&lt;/p&gt;

&lt;p&gt;A lower number means the car is more similar to our ideal profile:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Almost identical:&lt;/strong&gt; 0.00&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Very close:&lt;/strong&gt; 0.05&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Somewhat different:&lt;/strong&gt; 0.25&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quite different:&lt;/strong&gt; 0.45&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So in our results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The “Perfect high-performance hybrid” and “Sporty fuel-efficient coupe” are closest to the ideal (0.00).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The “Economy compact car” and “Luxury performance SUV” are also good matches but slightly further.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The “Old gas-guzzling truck” is farthest away, it’s the least similar in performance and fuel use.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This simple ranking shows the core idea behind AI-powered vector search: finding items that are &lt;em&gt;conceptually similar&lt;/em&gt;, not just textually identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  From SQL to Application Logic with PL/SQL
&lt;/h2&gt;

&lt;p&gt;SQL is for asking questions. But what about building application logic? For that, we turn to &lt;strong&gt;PL/SQL&lt;/strong&gt;, Oracle's native programming language that lives inside the database. It lets you use variables, create logic, and handle errors, turning the database into a powerful processing engine.&lt;/p&gt;

&lt;h3&gt;
  
  
  What is PL/SQL?
&lt;/h3&gt;

&lt;p&gt;PL/SQL (Procedural Language/SQL) was designed to overcome the limitations of pure SQL by adding procedural constructs—variables, loops, and error handling. Its key advantage is its tight integration with the SQL engine, which eliminates network overhead and results in significant performance gains for data-intensive operations.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Anatomy of PL/SQL
&lt;/h3&gt;

&lt;p&gt;If you're coming from a language like Python, the structure of a basic PL/SQL block will feel surprisingly familiar:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DECLARE&lt;/span&gt;
  &lt;span class="c1"&gt;-- This is your setup area where you declare variables.&lt;/span&gt;
&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="c1"&gt;-- This is your main execution block where logic runs.&lt;/span&gt;
&lt;span class="n"&gt;EXCEPTION&lt;/span&gt;
  &lt;span class="c1"&gt;-- This is your "try...except" block for handling errors.&lt;/span&gt;
&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  PL/SQL in Action: An Interactive Similarity Report
&lt;/h3&gt;

&lt;p&gt;Let's apply this structure to build a simple, interactive report. The script will ask you to pick a car's ID and then generate a similarity report comparing that car to our "Ideal Car Profile."&lt;/p&gt;

&lt;p&gt;Copy and paste this entire block into your worksheet and run it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;DECLARE&lt;/span&gt;
  &lt;span class="c1"&gt;-- 1. Our benchmark: the "Perfect Car Profile"&lt;/span&gt;
  &lt;span class="n"&gt;ideal_car_profile_vector&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'[10, 2]'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; 

  &lt;span class="c1"&gt;-- 2.  Variables to hold the data we fetch from the table&lt;/span&gt;
  &lt;span class="n"&gt;selected_car_vector&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="n"&gt;selected_car_desc&lt;/span&gt; &lt;span class="n"&gt;VARCHAR2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;-- 3. Variable to hold the calculated distance (similarity score)&lt;/span&gt;
  &lt;span class="n"&gt;similarity_score&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;-- 4. This will prompt the user to enter a car ID to compare (e.g. 2 for "Sporty fuel-efficient coupe")&lt;/span&gt;
  &lt;span class="n"&gt;car_id_to_check&lt;/span&gt; &lt;span class="n"&gt;NUMBER&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;car_id_to_compare&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;BEGIN&lt;/span&gt;
  &lt;span class="c1"&gt;-- Fetch the chosen car’s vector and description from the table&lt;/span&gt;
  &lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;performance_consumption_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;car_description&lt;/span&gt;
  &lt;span class="k"&gt;INTO&lt;/span&gt; &lt;span class="n"&gt;selected_car_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selected_car_desc&lt;/span&gt;
  &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;car_profiles&lt;/span&gt;
  &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;car_id_to_check&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;-- Calculate how far this car is from the ideal&lt;/span&gt;
  &lt;span class="n"&gt;similarity_score&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;VECTOR_DISTANCE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ideal_car_profile_vector&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;selected_car_vector&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;-- Display a readable report&lt;/span&gt;
  &lt;span class="n"&gt;DBMS_OUTPUT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PUT_LINE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'--- Car Similarity Report ---'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;DBMS_OUTPUT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PUT_LINE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Selected Car:      '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;selected_car_desc&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;DBMS_OUTPUT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PUT_LINE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Ideal Profile:     [10, 2] (High performance, Low fuel use)'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;DBMS_OUTPUT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PUT_LINE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Car&lt;/span&gt;&lt;span class="se"&gt;''&lt;/span&gt;&lt;span class="s1"&gt;s Profile:        '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;TO_CHAR&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;selected_car_vector&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="c1"&gt;-- TO_CHAR converts the vector into a readable string&lt;/span&gt;
  &lt;span class="n"&gt;DBMS_OUTPUT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PUT_LINE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Similarity Score:  '&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;similarity_score&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="n"&gt;DBMS_OUTPUT&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;PUT_LINE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'(A lower score means a better match!)'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;END&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="o"&gt;/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When you run this, it will prompt you: Enter value for &lt;strong&gt;car_id_to_compare:&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%2Fcbh9vn0f3kz9ce98p4ya.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%2Fcbh9vn0f3kz9ce98p4ya.png" alt="PROMPT"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The &lt;code&gt;&amp;amp;&lt;/code&gt; syntax is a feature of the SQL client that prompts you for input, allowing us to make the script interactive. And &lt;code&gt;||&lt;/code&gt; is used to concatenate strings. &lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Enter 2 and press &lt;code&gt;Enter&lt;/code&gt;. The script will fetch the "Sporty fuel-efficient coupe" and generate a report showing its low similarity score, proving it's a great match!&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%2Ffspay50bzvew22qjrl9w.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%2Ffspay50bzvew22qjrl9w.png" alt="RESULT"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;To make this even clearer, let's imagine a real-world project: building a "Similar Products" feature for an online bookstore.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Benchmark:&lt;/strong&gt; A customer is looking at the page for "Project Hail Mary," a popular science-fiction book. The vector for this book, which represents its genre, writing style, and core themes (space, problem-solving, humor), becomes our &lt;code&gt;ideal_profile_vector&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Candidates:&lt;/strong&gt; Your database contains vectors for thousands of other books.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Logic:&lt;/strong&gt; In the background, the application runs a process just like our simple PL/SQL script. It calculates the &lt;code&gt;VECTOR_DISTANCE&lt;/code&gt; between the "Project Hail Mary" vector and all other books in the catalog.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Result:&lt;/strong&gt; The books with the lowest distance score are displayed to the customer under a "You Might Also Like..." section. The system would correctly suggest other hard sci-fi novels or books with similar narrative styles, rather than a random cookbook or history textbook, because their "meaning coordinates" are closest.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Our simple, interactive script, which compares one benchmark to one new item, is the tiny engine that powers each one of those individual recommendations. By running that logic for many items, you build a complete feature.&lt;/p&gt;

&lt;h2&gt;
  
  
  Next Steps: Performance and Other Metrics
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Distance Metrics:&lt;/strong&gt; The default distance metric is &lt;code&gt;COSINE&lt;/code&gt;, which is excellent for semantic text search. For our coordinate-based example, &lt;code&gt;EUCLIDEAN&lt;/code&gt; (the straight-line distance) is also a great choice. You can specify it like this: &lt;code&gt;VECTOR_DISTANCE(v1, v2, EUCLIDEAN)&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Indexing for Speed:&lt;/strong&gt; Searching through millions of vectors requires an index. Just like a book index helps you find a topic instantly, a vector index helps the database find the "nearest neighbors" without a full table scan.&lt;br&gt;
This is critical for production applications, as it’s the difference between a sub-second response and a query that takes minutes on a large dataset.&lt;br&gt;
&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="n"&gt;VECTOR&lt;/span&gt; &lt;span class="k"&gt;INDEX&lt;/span&gt; &lt;span class="n"&gt;car_profiles_idx&lt;/span&gt; &lt;span class="k"&gt;ON&lt;/span&gt; &lt;span class="n"&gt;car_profiles&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;performance_consumption_vector&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ORGANIZATION&lt;/span&gt; &lt;span class="n"&gt;INMEMORY&lt;/span&gt; &lt;span class="n"&gt;NEIGHBOR&lt;/span&gt; &lt;span class="n"&gt;GRAPH&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion: Your Journey into AI on OCI
&lt;/h2&gt;

&lt;p&gt;Congratulations! You've just taken your first and most important step into the world of AI-powered search. &lt;br&gt;
You've seen that vectors are just coordinates for meaning, and that Oracle makes it incredibly simple to store, manage, and query them using familiar SQL and PL/SQL.&lt;/p&gt;

&lt;p&gt;This skill is a cornerstone for building modern, intelligent applications. Whether you're working with text, images, or any other unstructured data, the ability to find "conceptually similar" items is a superpower. By integrating these AI capabilities directly into the database, Oracle Cloud Infrastructure (OCI) provides a powerful, scalable, and secure platform for your next generation of smart applications. Keep experimenting! Happy Building! ☁️&lt;/p&gt;
&lt;h3&gt;
  
  
  The Big Picture: Technical Architecture
&lt;/h3&gt;

&lt;p&gt;While our example focused on the core SQL and PL/SQL commands, it's helpful to see how AI Vector Search fits into a larger application architecture. The diagram below from Oracle's official documentation illustrates how an application, the database, and embedding models work together to provide similarity search on your data.&lt;/p&gt;
&lt;h4&gt;
  
  
  Oracle AI Vector Search Technical Architecture Diagram
&lt;/h4&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%2Fr73hpbjnll41230t1dmq.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%2Fr73hpbjnll41230t1dmq.png" alt="OCI Architecture"&gt;&lt;/a&gt;&lt;br&gt;
Source: &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vsiad/aivs_genarch.html" rel="noopener noreferrer"&gt;Oracle&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  Next Steps: Generating Vectors in Your Application
&lt;/h3&gt;

&lt;p&gt;In this guide, we manually created vectors to focus on the search functionality. In a real-world application, you would use an AI "embedding model" to automatically generate these vectors from your data (like text descriptions, image pixels, or other attributes).&lt;/p&gt;

&lt;p&gt;Ready to take the next step? The official Oracle documentation provides a detailed guide: &lt;a href="https://docs.oracle.com/en/database/oracle/oracle-database/26/vecse/get-started-node.html" rel="noopener noreferrer"&gt;Oracle AI Vector Search User's Guide&lt;/a&gt;. This is the perfect follow-up to what we've covered here.&lt;/p&gt;

&lt;p&gt;

  &lt;iframe src="https://www.youtube.com/embed/pu79sny1AzY"&gt;
  &lt;/iframe&gt;


&lt;/p&gt;




&lt;p&gt;Cover Photo by &lt;a href="https://unsplash.com/@boliviainteligente?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;BoliviaInteligente&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/text-_BFJOg1nxaw?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>oracle</category>
      <category>ai</category>
      <category>database</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Cloud Architects are Movie Directors: An OCI IAM Story</title>
      <dc:creator>Smyekh David-West</dc:creator>
      <pubDate>Thu, 02 Oct 2025 00:36:02 +0000</pubDate>
      <link>https://dev.to/smyekh/cloud-architects-are-movie-directors-an-oci-iam-story-4lni</link>
      <guid>https://dev.to/smyekh/cloud-architects-are-movie-directors-an-oci-iam-story-4lni</guid>
      <description>&lt;h2&gt;
  
  
  ☁️ Pre-Flight Checklist
&lt;/h2&gt;

&lt;p&gt;Before we taxi down the runway, here’s your flight plan. Keep this handy to navigate your flight path. Welcome aboard the cloud!&lt;/p&gt;

&lt;h3&gt;
  
  
  🌥️ Takeoff
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Introduction: The Blank Set&lt;/li&gt;
&lt;li&gt;The Film Crew: Deconstructing Cloud Roles&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⛅️ Cruising Altitude
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Cast (OCI Principals)&lt;/li&gt;
&lt;li&gt;The Set: Where the Action Happens (Compartments)&lt;/li&gt;
&lt;li&gt;
The Script (OCI Policies)
A Scene in Action: A Practical Walkthrough
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌤️ Landing &amp;amp; Taxi
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Bad Directing: Common IAM Pitfalls&lt;/li&gt;
&lt;li&gt;Conclusion: A Secure and Efficient Production&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enjoy your flight! ☁️&lt;/p&gt;




&lt;h2&gt;
  
  
  Introduction: The Blank Set
&lt;/h2&gt;

&lt;p&gt;Starting with a new Oracle Cloud Infrastructure (OCI) tenancy is like standing on an empty movie set. It's a space full of potential—powerful compute, vast storage, and advanced services—but nothing can happen on its own. The lights are off, the cameras aren't rolling, and the actors have no script. This is the core of cloud security: by default, nothing is allowed.&lt;/p&gt;

&lt;p&gt;So, how do you safely and efficiently bring a complex application to life in this environment? How do you control who does what, what they can interact with, and what they can't?&lt;/p&gt;

&lt;p&gt;The "Aha!" moment comes when you stop thinking about resources as abstract blocks and start thinking of every user and every resource as an actor in a grand production.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Film Crew: Deconstructing Cloud Roles
&lt;/h2&gt;

&lt;p&gt;In modern software development, titles like "Architect," "Developer," and "DevOps Engineer" are often intertwined, leading to confusion. Our movie production analogy helps clarify their distinct but collaborative functions.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Director (The Cloud Architect):&lt;/strong&gt; The Director is the visionary. They are responsible for the overall plot, the characters' motivations, and the story's structure. In the cloud, the Architect designs the master plan—the application architecture, the network layout, the data flows, and, most importantly, the high-level security and IAM strategy. They write the master "script" that governs the entire production.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Actors &amp;amp; Artisans (The Developers):&lt;/strong&gt; These are the performers and builders who bring the Director's vision to life. They write the dialogue (code), build the props (features), and act out the scenes (run the application logic). A great developer needs to understand the Director's intent (the architecture) to deliver a compelling and coherent performance.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The Production Crew (The DevOps Engineers):&lt;/strong&gt; This crew is the master of logistics and automation. They are the stunt coordinators, special effects supervisors, and camera operators. They build and manage the machinery that makes filming efficient and repeatable (CI/CD pipelines), automate the setup and teardown of complex sets (Infrastructure as Code), and ensure the entire production runs smoothly and safely (operations and monitoring). They are the critical bridge between the Director's vision and the practical reality of shooting the film.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;While their roles are different, their skills are intertwined. A good Director understands the acting process, and a great Actor knows a bit about directing. Likewise, a great cloud professional understands all three perspectives.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Cast (OCI Principals)
&lt;/h2&gt;

&lt;p&gt;In our OCI production, there are two types of actors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Human Actors (Users and Groups):&lt;/strong&gt; These are the people: the administrators, developers, and operators. To make them manageable, you don't give them scripts individually. Instead, you cast them into roles like "Lighting Crew" or "Sound Engineers." In OCI, these roles are &lt;strong&gt;Groups&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Automated Actors (Resources and Dynamic Groups):&lt;/strong&gt; These are the non-human actors—the "robots," "drones," or "special effects" that perform tasks automatically. You cast these actors into roles using &lt;strong&gt;Dynamic Groups&lt;/strong&gt;, which automatically group resources based on defined rules (e.g., all instances in a specific compartment).&lt;/p&gt;

&lt;p&gt;The key question to ask is: &lt;strong&gt;"Does this resource need to make its own authenticated API calls to other OCI services?"&lt;/strong&gt; If yes, it's an automated actor. Here are some common examples:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Compute Instances (and Docker):&lt;/strong&gt; A standard virtual machine is the most common type of actor. If this VM is running Docker containers, the permissions granted to the VM can be used by the processes within those containers.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;OKE Worker Nodes:&lt;/strong&gt; The VMs in a Kubernetes cluster are perfect candidates for a Dynamic Group. This allows you to grant permissions to all your worker nodes at once, so the applications they run can securely access other OCI services like databases or object storage.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;OCI Functions:&lt;/strong&gt; A Function is a prime example of an actor. When triggered, it might need to read a file or update a database. Placing the Function in a Dynamic Group is the standard way to grant it these permissions.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;API Gateway:&lt;/strong&gt; A Gateway can also be an actor. It might need to invoke a private backend Function or another service. Placing the Gateway in a Dynamic Group allows it to do so securely.&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Set: Where the Action Happens (Compartments)
&lt;/h2&gt;

&lt;p&gt;You can't film a movie in a single, giant warehouse. You need different sets: a castle, a spaceship, a city street. In OCI, these sets are &lt;strong&gt;Compartments&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Compartments are the primary way to organize and isolate your cloud resources. They are the foundational building blocks for a secure production. Instead of having one giant, confusing script for the whole studio, the Director can apply specific rules to each set. For example, the "Stunt-Actors" group might be allowed to use "pyrotechnics" (powerful resources) but only on the "Action-Scene" set (a specific compartment).&lt;/p&gt;

&lt;p&gt;Organizing resources into compartments (e.g., by environment, project, or department) is the first and most critical step in building a manageable and secure cloud.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Script (OCI Policies)
&lt;/h2&gt;

&lt;p&gt;The script dictates every single action and interaction in the production. In OCI, the script is your set of &lt;strong&gt;IAM Policies&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;This script is built on one fundamental rule: the &lt;strong&gt;Principle of Least Privilege&lt;/strong&gt;. By default, no actor can do anything. The set is dark and silent. An action can only happen if the Director explicitly writes an &lt;code&gt;allow&lt;/code&gt; statement in the script.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;code&gt;Allow group 'Developers' to manage instance-family in compartment 'Development'&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This line tells a specific cast of actors (the 'Developers' group) that they are allowed to perform a specific set of actions ('manage instance-family') on a specific set ('Development' compartment).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;The Studio Head (The Tenancy Administrator):&lt;/strong&gt; This is a special role, like the head of the movie studio. They have the ultimate power to approve any script and fund any production. However, they are too busy to be involved in every scene. This role should be used sparingly, as the Director (Architect) should be the one crafting the script for the actual production.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A Scene in Action: A Practical Walkthrough
&lt;/h2&gt;

&lt;p&gt;Let's tie it all together with a real-world scene.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;The Scene:&lt;/strong&gt; A user uploads their profile picture to a web application. The application backend, running on a Compute VM, needs to save this picture to an Object Storage bucket.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The Locations (Compartments):&lt;/strong&gt; The Compute VM lives in a compartment named &lt;code&gt;App-Prod&lt;/code&gt;. The Object Storage bucket, named &lt;code&gt;ProfilePics&lt;/code&gt;, lives in a compartment named &lt;code&gt;Assets-Prod&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The Casting (Dynamic Group):&lt;/strong&gt; We can't give permissions to a single VM; that doesn't scale. Instead, we create a rule to automatically cast all VMs in our app compartment into a role.

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Dynamic Group Name:&lt;/strong&gt; &lt;code&gt;WebAppServers&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Matching Rule:&lt;/strong&gt; &lt;code&gt;All {instance.compartment.id = 'ocid1.compartment.oc1..exampleuniqueID'}&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;li&gt;  &lt;strong&gt;The Script (Policy):&lt;/strong&gt; Now, the Director writes a very specific line in the script. This policy is attached to the &lt;code&gt;Assets-Prod&lt;/code&gt; compartment.

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;Allow dynamic-group 'WebAppServers' to manage objects in compartment 'Assets-Prod' where target.bucket.name = 'ProfilePics'&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;This policy is precise. It doesn't just allow any web server to write to any bucket. It says &lt;em&gt;only&lt;/em&gt; the actors in the &lt;code&gt;WebAppServers&lt;/code&gt; role can manage objects, and &lt;em&gt;only&lt;/em&gt; in the specific &lt;code&gt;ProfilePics&lt;/code&gt; bucket. That's good directing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bad Directing: Common IAM Pitfalls
&lt;/h2&gt;

&lt;p&gt;Even the best directors can make mistakes, especially on their first film. Here are some common IAM anti-patterns framed as "bad directing."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Giving an Actor Keys to the Whole Studio:&lt;/strong&gt; This is writing an overly broad policy, like &lt;code&gt;Allow group 'Developers' to manage all-resources in tenancy&lt;/code&gt;. This is a security nightmare. It's lazy directing and leads to chaos.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Shooting a Movie in One Big Warehouse:&lt;/strong&gt; This is failing to use compartments, instead putting every single resource into the root compartment. The set is chaotic, it's impossible to know who should be where, and writing a coherent script becomes impossible.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Casting the Studio Head in Every Role:&lt;/strong&gt; This is using the highly-privileged tenancy administrator account for everyday tasks like development or testing. The Studio Head is powerful but should not be on set. Using this account for daily work dramatically increases the risk of a catastrophic mistake.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Conclusion: A Secure and Efficient Production
&lt;/h2&gt;

&lt;p&gt;Thinking of yourself as a Director changes your perspective. You are no longer just connecting boxes; you are scripting a performance. You are forced to think deliberately about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Who&lt;/strong&gt; are my actors? (Users and Resources)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;What&lt;/strong&gt; are their roles? (Groups and Dynamic Groups)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Where&lt;/strong&gt; can they do it? (Compartments)&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;What&lt;/strong&gt; should they be allowed to do? (Policies)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This "director's mindset" is the key to moving beyond a chaotic, insecure collection of resources and creating a well-organized, secure, and efficient cloud environment.&lt;/p&gt;




&lt;p&gt;Cover Photo by &lt;a href="https://unsplash.com/@boliviainteligente?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;BoliviaInteligente&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/text-_BFJOg1nxaw?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>oci</category>
      <category>cloud</category>
      <category>security</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Completing Your Local OCI Lab: A Guide to Port Forwarding in UTM</title>
      <dc:creator>Smyekh David-West</dc:creator>
      <pubDate>Thu, 18 Sep 2025 11:30:00 +0000</pubDate>
      <link>https://dev.to/smyekh/completing-your-local-oci-lab-a-guide-to-port-forwarding-in-utm-hgp</link>
      <guid>https://dev.to/smyekh/completing-your-local-oci-lab-a-guide-to-port-forwarding-in-utm-hgp</guid>
      <description>&lt;h3&gt;
  
  
  ☁️ Pre-Flight Checklist
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;This is a connecting flight&lt;/strong&gt;. Before we taxi down the runway, here’s your flight plan. Keep this handy to navigate your flight path. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Welcome aboard the cloud!&lt;/strong&gt; ☁️&lt;/p&gt;

&lt;h3&gt;
  
  
  🌥️ Takeoff
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Why You Need Port Forwarding&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⛅️ Cruising Altitude
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;How to Set Up Port Forwarding in UTM (macOS + Linux VM)&lt;/li&gt;
&lt;li&gt;How Port Forwarding Relates to OCI Networking&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌤️ Landing &amp;amp; Taxi
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Enjoy your flight! ☁️&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;So far in our series, we've built a local, production-like Ubuntu VM and bridged it to our Mac with a shared folder. We have a clean environment and a seamless coding workflow. Now, we face the final hurdle: networking.&lt;/p&gt;

&lt;p&gt;You've written a web application, and you're running it inside the VM. How do you test its API from your Mac? How do you debug the frontend in your browser? By default, the VM is a black box.&lt;/p&gt;

&lt;p&gt;This is where port forwarding comes in. It’s not just a local convenience; it’s a fundamental skill that helps you understand and debug one of the most critical components of OCI: Security Lists and Network Security Groups (NSGs). This guide will show you how to configure port forwarding in UTM to complete your local OCI lab.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why You Need Port Forwarding
&lt;/h2&gt;

&lt;p&gt;UTM's default Shared Network mode acts like a home router for your VM. It uses Network Address Translation (NAT) to give your VM internet access while hiding it behind a virtual firewall. This is great for isolation, but it means your Mac can't directly see the services (or "ports") that your VM opens.&lt;/p&gt;

&lt;p&gt;Port forwarding is like creating a special rule on that virtual router. It tells UTM, "Any traffic that arrives at this specific port on my Mac should be forwarded directly to that specific port inside my VM."&lt;/p&gt;

&lt;p&gt;You need it to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Access a web server in the VM from your Mac's browser.&lt;/li&gt;
&lt;li&gt;SSH into your VM from your Mac's terminal.&lt;/li&gt;
&lt;li&gt;Connect to a database or API running in the VM.&lt;/li&gt;
&lt;li&gt;Develop with frameworks like React, Flask, or FastAPI inside the VM and view the results on your Mac.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How to Set Up Port Forwarding in UTM (macOS + Linux VM)
&lt;/h2&gt;

&lt;p&gt;In these steps, I’ll show you &lt;strong&gt;exactly how to set up port forwarding in UTM&lt;/strong&gt;, test it with a simple Python web server, and troubleshoot common issues.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 1: Configure Port Forwarding in UTM
&lt;/h3&gt;

&lt;p&gt;Make sure the VM is &lt;strong&gt;powered off&lt;/strong&gt; before making network changes. Port forwarding changes can’t be made while the VM is running.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Open UTM and Select Your VM
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;Launch &lt;strong&gt;UTM&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In the main UTM window, select your Ubuntu VM from the list on the
left.&lt;/li&gt;
&lt;li&gt;Right-click and select the edit option to open the VM's configuration.&lt;/li&gt;
&lt;/ol&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%2Fgabmio3b1zp5j53v3sr1.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%2Fgabmio3b1zp5j53v3sr1.png" alt="How to set up port forwarding" width="800" height="579"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Configure the Network
&lt;/h3&gt;

&lt;h4&gt;
  
  
  1. In the &lt;strong&gt;“Network”&lt;/strong&gt; tab:
&lt;/h4&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%2F5mv47sl4q48up91x1his.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%2F5mv47sl4q48up91x1his.png" alt="How to set up port forwarding" width="800" height="582"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  2. Set &lt;strong&gt;Network Mode&lt;/strong&gt; to Emulated VLAN (Shared Network) (sometimes labelled as “NAT”). Once you do that, a &lt;em&gt;Port Forward&lt;/em&gt; option will appear below Network
&lt;/h4&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%2Fbe57kwoah265auwqxkk4.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%2Fbe57kwoah265auwqxkk4.png" alt="How to set up port forwarding" width="800" height="574"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  3. Add a Port Forwarding Rule:
&lt;/h4&gt;

&lt;p&gt;Click New and then set:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Field&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Example&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Protocol&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;TCP&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Host Address&lt;/strong&gt; for localhost access) or leave blank for all interfaces.&lt;/td&gt;
&lt;td&gt;127.0.0.1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Host Port&lt;/strong&gt; (Port on &lt;strong&gt;your Mac)&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;8080 (or any open port)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Guest Address&lt;/strong&gt; (IP of the VM)&lt;/td&gt;
&lt;td&gt;use 10.0.2.15 or just leave blank to auto-select&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;Guest Port&lt;/strong&gt; (Port on the &lt;strong&gt;VM)&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;80 (or your app’s port)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2F4nhxk73sc3gojhpftcpp.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%2F4nhxk73sc3gojhpftcpp.png" alt="How to set up port forwarding" width="800" height="542"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Example: Forward Mac’s &lt;code&gt;localhost:8080&lt;/code&gt; → VM’s &lt;code&gt;port 80&lt;/code&gt; (where a web server will run).&lt;/p&gt;

&lt;p&gt;Use the top labels at the top (i.e., Protocol, Guest Address, Guest Port) to guide you for your entries.&lt;/p&gt;

&lt;p&gt;Then save&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: Run a Web Server Inside the VM
&lt;/h3&gt;

&lt;p&gt;Boot into your Linux VM and run:&lt;/p&gt;

&lt;p&gt;Install Python (if needed):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt update
sudo apt install python3 -y
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start a test HTTP server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo python3 -m http.server 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Port 80 requires sudo. You can also use python3 -m http.server 8080 and adjust the port forwarding accordingly.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Step 5: Test from macOS
&lt;/h3&gt;

&lt;p&gt;From your Mac Terminal, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl http://localhost:8080
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see an HTML directory listing from the VM — success! Your Mac connected to the VM’s internal web server via &lt;code&gt;localhost:8080&lt;/code&gt;, routed through UTM’s NAT — exactly as intended.&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%2F7shqthulkn98dh28c8lb.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%2F7shqthulkn98dh28c8lb.png" alt="How to set up port forwarding" width="800" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you &lt;code&gt;curl http://localhost:8080&lt;/code&gt; from your Mac without starting the test server, you get &lt;code&gt;curl: (56) Recv failure: Connection reset by peer&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Why You’re Seeing a Directory Listing&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;That’s the default behaviour of:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python3 -m http.server 80
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It serves the &lt;strong&gt;current directory’s contents&lt;/strong&gt; over HTTP. Since you’re likely in &lt;code&gt;/&lt;/code&gt; (the root directory) and there are no files there (or permissions restrict listing), the &lt;code&gt;&amp;lt;ul&amp;gt;&lt;/code&gt; is empty.&lt;/p&gt;

&lt;h3&gt;
  
  
  Troubleshooting Summary
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Problem&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Fix&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;curl: (7) Failed to connect&lt;/td&gt;
&lt;td&gt;Ensure the VM server is running and port forwarding is saved&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PermissionError: [Errno 13]&lt;/td&gt;
&lt;td&gt;Use sudo for ports below 1024&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Nothing shows in the browser.&lt;/td&gt;
&lt;td&gt;Try forwarding to guest port 8080 instead of 80&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Bonus: Forward Other Services&lt;/strong&gt;
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Service&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Guest Port&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Host Port&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Notes&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;SSH&lt;/td&gt;
&lt;td&gt;22&lt;/td&gt;
&lt;td&gt;2222&lt;/td&gt;
&lt;td&gt;ssh user@localhost -p 2222&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Flask app&lt;/td&gt;
&lt;td&gt;5000&lt;/td&gt;
&lt;td&gt;5000&lt;/td&gt;
&lt;td&gt;Run with host='0.0.0.0'&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JupyterLab&lt;/td&gt;
&lt;td&gt;8888&lt;/td&gt;
&lt;td&gt;8888&lt;/td&gt;
&lt;td&gt;Run with Jupyter Lab --ip=0.0.0.0.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;FastAPI&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;8000&lt;/td&gt;
&lt;td&gt;8000&lt;/td&gt;
&lt;td&gt;Run with uvicorn app:app --host 0.0.0.0 --port 8000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;React (Vite)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;5173&lt;/td&gt;
&lt;td&gt;5173&lt;/td&gt;
&lt;td&gt;Run with npm run dev or vite --host (or set "host": true in config)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;React (CRA)&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;td&gt;3000&lt;/td&gt;
&lt;td&gt;npm start — override with .env: HOST=0.0.0.0 if needed&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;With port forwarding configured, your VM is no longer an isolated box. It's a fully integrated, powerful, and safe extension of your local development environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Port Forwarding Relates to OCI Networking
&lt;/h2&gt;

&lt;p&gt;In OCI, nothing is accessible to the outside world unless you explicitly open a port in a Security List or NSG. Getting a "connection timed out" error because a firewall rule is missing is one of the most common issues developers face.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Port forwarding in UTM is the local equivalent of an OCI ingress rule.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The Problem: Your service is running, but you can't connect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The OCI Solution: You add an ingress rule to your VCN's Security List for the destination port (e.g., &lt;code&gt;allow TCP traffic on port 443 from source 0.0.0.0/0&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The Local VM Solution: You add a port forwarding rule in UTM (e.g., forward host port 8443 to guest port 443).&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;By practising this locally, you build the muscle memory for cloud networking. You learn to think about which ports your application needs and how to expose them, dramatically speeding up your debugging process when you deploy to a real OCI environment. It turns an abstract networking rule into a tangible, hands-on concept.&lt;/p&gt;

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

&lt;p&gt;With port forwarding configured, your local lab is complete. You have successfully built an isolated, production-mirroring environment that you can code in and network with, all from the comfort of your Mac.&lt;/p&gt;

&lt;p&gt;This three-part journey of setting up the VM, creating a shared folder, and configuring port forwarding was about more than just tools. It was about adopting the workflow of a professional cloud engineer. The initial effort to build this environment pays for itself tenfold in saved time, fewer errors in production, and a deeper understanding of how cloud infrastructure truly works. You've eliminated "it works on my machine" from your vocabulary for good.&lt;/p&gt;




&lt;p&gt;Cover Photo by &lt;a href="https://unsplash.com/@boliviainteligente?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;BoliviaInteligente&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/text-_BFJOg1nxaw?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>utm</category>
      <category>networking</category>
      <category>ssh</category>
      <category>oci</category>
    </item>
    <item>
      <title>The OCI Developer's Workflow: Bridging Your Mac and Local VM with a Shared Folder</title>
      <dc:creator>Smyekh David-West</dc:creator>
      <pubDate>Mon, 15 Sep 2025 10:52:26 +0000</pubDate>
      <link>https://dev.to/smyekh/the-oci-developers-workflow-bridging-your-mac-and-local-vm-with-a-shared-folder-34ic</link>
      <guid>https://dev.to/smyekh/the-oci-developers-workflow-bridging-your-mac-and-local-vm-with-a-shared-folder-34ic</guid>
      <description>&lt;h3&gt;
  
  
  ☁️ Pre-Flight Checklist
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;This is a connection flight&lt;/strong&gt;. Before we taxi down the runway, here’s your flight plan. Keep this handy to navigate your flight path. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Welcome aboard the cloud!&lt;/strong&gt; ☁️&lt;/p&gt;

&lt;h3&gt;
  
  
  🌥️ Takeoff
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;The Goal: Edit on Mac, Run in Linux&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  ⛅️ Cruising Altitude
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;How to Set Up a Shared Folder in UTM (macOS to Ubuntu 25.0 VM)&lt;/li&gt;
&lt;li&gt;How This Workflow Benefits OCI Development&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🌤️ Landing &amp;amp; Taxi
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Conclusion&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Enjoy your flight! ☁️&lt;/strong&gt;&lt;/p&gt;




&lt;p&gt;In our previous article, we established why running a local Ubuntu VM is a game-changer for any serious OCI professional. You now have a pristine, production-mirroring environment. But there's a problem: it's isolated. How do you get your Terraform scripts, application code, or Ansible playbooks from your Mac into the VM without a clunky, manual process?&lt;/p&gt;

&lt;p&gt;This is where a shared folder becomes the critical bridge in your local OCI lab. It’s not just about convenience; it’s about simulating the deployment and testing workflow of a real cloud environment. You need a way to edit files on your host and immediately test them in a Linux environment that mirrors your OCI instances.&lt;/p&gt;

&lt;p&gt;This guide will show you how to set up a persistent, high-performance shared folder using UTM and bindfs, solving the tricky permissions issues along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Goal: Edit on Mac, Run in Linux
&lt;/h2&gt;

&lt;p&gt;Our objective is to create a folder on your Mac that appears inside your Ubuntu VM as a native directory. You'll be able to use your favourite macOS editor (like VS Code) to write code and instantly compile, run, and test it within the Linux environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to Set Up a Shared Folder in UTM (macOS to Ubuntu 25.0 VM)
&lt;/h2&gt;

&lt;p&gt;These steps will walk you through the process of sharing a folder from your macOS host to your Ubuntu guest VM. This allows you to edit code and manage files on your Mac and have them instantly accessible inside&lt;br&gt;
the VM.&lt;/p&gt;
&lt;h3&gt;
  
  
  Part 1: Create a Shared Folder on Your Mac (Host)
&lt;/h3&gt;

&lt;p&gt;First, we need a dedicated folder on your Mac that you want to share with the VM.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Finder and navigate to where you want to create the folder. For this guide, we'll use the desktop.&lt;/li&gt;
&lt;li&gt;Create a new folder. You can name it whatever you like, but for this example, we'll call it UTM_Share.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;Tip: You can also do this from the terminal on your mac&lt;br&gt;
&lt;/p&gt;


&lt;/blockquote&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd `/Desktop
mkdir UTM_share
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;(Optional) Place a test file inside the '&lt;strong&gt;UTM_Share&lt;/strong&gt;' folder so we can confirm the connection later. You can create a simple text file named &lt;code&gt;hello_from_mac.txt&lt;/code&gt;.&lt;/p&gt;


&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cd UTM_share
touch hello_from_mac.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Part 2: Configure Sharing in UTM
&lt;/h3&gt;

&lt;p&gt;Next, we need to tell UTM which folder to share and what to call it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; You must shut down your virtual machine before changing its hardware settings, including shared directories.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ol&gt;
&lt;li&gt;Shut down your Ubuntu VM.&lt;/li&gt;
&lt;li&gt;In the main UTM window, select your Ubuntu VM from the list on the left.&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Right-click and select the edit option to open the VM's configuration.&lt;br&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%2Fs8rp5a5ixasrj1o7tdg9.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%2Fs8rp5a5ixasrj1o7tdg9.png" alt="How to set up a shared folder in utm" width="800" height="579"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Navigate to the Sharing tab from the list on the left.&lt;br&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%2F7g1q5nf5pveo4jzd7i4v.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%2F7g1q5nf5pveo4jzd7i4v.png" alt="How to set up a shared folder in utm" width="800" height="576"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click on the browse button to add a new shared directory.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Navigate to and select the UTM_Share folder you created on your Desktop.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;The "Name" field will auto-populate with UTM_Share. This is the "mount tag" we will use inside the VM. You can leave it as is.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ensure the "Read Only" checkbox is unchecked if you want to write files from within the VM.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Click Save to close the VM settings.&lt;br&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%2Fevdgist80be0s4ndunpc.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%2Fevdgist80be0s4ndunpc.png" alt="How to set up a shared folder in utm" width="800" height="577"&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Part 3: Mount the Shared Folder in Ubuntu (Guest)
&lt;/h3&gt;

&lt;p&gt;Now, we'll start the VM and mount the shared directory so we can access the files.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Start your Ubuntu VM and login
&lt;/h4&gt;

&lt;h4&gt;
  
  
  2. Open a terminal and create a mount point:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo mkdir -p /mnt/utm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Open &lt;code&gt;/etc/fstab&lt;/code&gt; with a text editor:
&lt;/h4&gt;

&lt;p&gt;Open your fstab file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nano /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add the following to the end&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# UTM Shared Folder
share /mnt/utm 9p trans=virtio,version=9p2000.L,rw,_netdev,nofail,auto 0 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save with &lt;code&gt;Crtl + O&lt;/code&gt;, press &lt;code&gt;Enter&lt;/code&gt;, then &lt;code&gt;Ctrl + X&lt;/code&gt;.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;strong&gt;TROUBLESHOOT STEP&lt;/strong&gt;
&lt;/h4&gt;

&lt;p&gt;&lt;strong&gt;In the UTM docs you’re told to follow this step after:&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Apply Mount Now (Without Reboot)&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo systemctl restart network-fs.target
systemctl list-units --type=mount
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ls /mnt/utm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But you may get this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Failed to restart network-fs.target: Unit network-fs.target not found.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is expected in some modern Ubuntu setups (especially 25.04). It’s not a blocker at all. Since network-fs.target isn’t available, just mount manually to confirm it works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo mount /mnt/utm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; &lt;span class="nt"&gt;-na&lt;/span&gt; /mnt/utm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;drwxr-xr-x  3 502 20  96 Jun 26 20:31 .
-rw-r--r--  1 502 20  14 Jun 26 20:30 test.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This confirms the shared Mac folder is mounted. &lt;/p&gt;

&lt;h4&gt;
  
  
  4. Apply the new fstab rules:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl daemon-reexec
sudo mount /mnt/utm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  5. Verify the mount:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ls -na /mnt/utm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see files like &lt;code&gt;hello_from_mac.txt&lt;/code&gt; with UID (e.g., 501) that differs from your linux user.&lt;/p&gt;

&lt;h3&gt;
  
  
  Part 4: Fixing Permissions Using bindfs
&lt;/h3&gt;

&lt;p&gt;The shared files may not be writable by your Linux user (usually UID 1000). By default, files in /mnt/utm may be owned by macOS UID 501 and GID 20, which doesn’t match your Ubuntu user’s UID/GID (usually 1000:1000). You might get permission denied errors when writing files. Use bindfs to remap ownership.&lt;/p&gt;

&lt;h4&gt;
  
  
  1. Install bindfs:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt update
sudo apt install bindfs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Check UID and GID:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id -u   # should return 1000
id -g   # should return 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  3. Check UID/GID of /mnt/utm:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ls -na /mnt/utm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for numbers like 501 (UID) and 20 (GID).&lt;/p&gt;

&lt;h4&gt;
  
  
  4. Create a remapped mount directory:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir ~/utm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  5. Add this second mount to /etc/fstab:
&lt;/h4&gt;

&lt;p&gt;Open your fstab file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo nano /etc/fstab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add this line:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# bindfs mount to remap UID/GID
/mnt/utm /home/smyekh/utm fuse.bindfs map=501/1000:@20/@1000,x-systemd.requires=/mnt/utm,_netdev,nofail,auto 0 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ Replace ‘&lt;strong&gt;smyekh&lt;/strong&gt;’ with your actual Linux username if different.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;ul&gt;
&lt;li&gt;map=501/1000 remaps UID 501 → 1000&lt;/li&gt;
&lt;li&gt;@20/&lt;a class="mentioned-user" href="https://dev.to/1000"&gt;@1000&lt;/a&gt; remaps GID 20 → 1000&lt;/li&gt;
&lt;li&gt;x-systemd.requires=/mnt/utm ensures it’s mounted &lt;strong&gt;after&lt;/strong&gt; /mnt/utm&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Save (&lt;code&gt;Ctrl + O&lt;/code&gt;, &lt;code&gt;Enter&lt;/code&gt;, &lt;code&gt;Ctrl + X&lt;/code&gt;)&lt;/p&gt;

&lt;h4&gt;
  
  
  6. Reload and mount:
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo systemctl daemon-reload
sudo mount /home/smyekh/utm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;⚠️ Again replace ‘&lt;strong&gt;smyekh&lt;/strong&gt;’ with your actual username.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you get an error saying the mount point is not empty:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ls -A ~/utm
# If it contains files, it's because /mnt/utm was already populated.
# This confirms sharing is working. You can safely continue.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  7. Final Verification
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;echo "Synced via bindfs!" &amp;gt; ~/utm/test_from_linux.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check your Mac's &lt;code&gt;UTM_Share&lt;/code&gt; folder, &lt;code&gt;test_from_linux.txt&lt;/code&gt; should be there!&lt;/p&gt;

&lt;h3&gt;
  
  
  Troubleshooting Summary
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Issue&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Fix&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;permission denied when writing to /mnt/utm&lt;/td&gt;
&lt;td&gt;Use bindfs to remap UID/GID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;network-fs.target not found&lt;/td&gt;
&lt;td&gt;Ignore; not critical for UTM/9p&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mount: mountpoint is not empty&lt;/td&gt;
&lt;td&gt;It’s fine if files are present already&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No shared files visible&lt;/td&gt;
&lt;td&gt;Confirm UTM Sharing settings and folder path&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Files don’t appear on Mac&lt;/td&gt;
&lt;td&gt;Ensure writing to ~/utm and not directly to /mnt/utm&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You now have a fully working shared folder between macOS and Ubuntu using UTM’s VirtFS and bindfs to handle permissions. This setup is persistent across reboots and ideal for syncing files, sharing projects, and bridging workflows between host and guest environments. &lt;/p&gt;

&lt;p&gt;I used the &lt;a href="https://docs.getutm.app/guest-support/linux/#virtfs" rel="noopener noreferrer"&gt;UTM documentation&lt;/a&gt; (official UTM VirtFS guide), which explains how to mount shared folders using the 9p filesystem (9pfs) via VirtIO.&lt;/p&gt;

&lt;h2&gt;
  
  
  How This Workflow Benefits OCI Development
&lt;/h2&gt;

&lt;p&gt;Simulating Code Deployment: In OCI, you don't code directly on the production server. You write code locally and deploy it. A shared folder perfectly mimics this: your Mac is your workstation, and the ~/utm directory in your VM is the "deployed" target. &lt;/p&gt;

&lt;p&gt;Testing Infrastructure as Code (IaC): Imagine you're writing a Terraform script to provision an OCI network. You can edit the .tf files in VS Code on your Mac, and because the folder is shared, you can immediately run terraform plan inside the VM. The VM has the OCI CLI and a Linux environment, ensuring your script behaves exactly as it will in a CI/CD pipeline or a bastion host.&lt;/p&gt;

&lt;p&gt;Developing and Testing Applications: You can write your Python, Go, or Node.js application on your Mac, and the code is instantly available in the VM to be compiled, run, and tested against Linux-specific dependencies.&lt;/p&gt;

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

&lt;p&gt;This setup combines the best of both worlds: the rich user interface of macOS and the production-parity environment of your Linux VM. Happy coding&lt;/p&gt;

&lt;p&gt;You've now engineered more than just a shared folder; you've built a professional workflow. This setup combines the rich UI of macOS with the production-parity environment of your Linux VM, creating a tight feedback loop for developing and testing OCI solutions.&lt;/p&gt;

&lt;p&gt;But what about networking? In our next and final guide in this series, we'll tackle the last piece of the puzzle: port forwarding. We'll show you how to access web servers and other services running inside your VM directly from your Mac, completing your local OCI lab setup.&lt;/p&gt;




&lt;p&gt;Cover Photo by &lt;a href="https://unsplash.com/@boliviainteligente?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;BoliviaInteligente&lt;/a&gt; on &lt;a href="https://unsplash.com/photos/text-_BFJOg1nxaw?utm_content=creditCopyText&amp;amp;utm_medium=referral&amp;amp;utm_source=unsplash" rel="noopener noreferrer"&gt;Unsplash&lt;/a&gt;&lt;/p&gt;

</description>
      <category>utm</category>
      <category>virtualisation</category>
      <category>devops</category>
      <category>ubuntu</category>
    </item>
  </channel>
</rss>
