<?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: Javi Palacios</title>
    <description>The latest articles on DEV Community by Javi Palacios (@fj_palacios).</description>
    <link>https://dev.to/fj_palacios</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.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3958394%2F53300b47-6c71-4239-a646-2a8cc4b00d1e.jpeg</url>
      <title>DEV Community: Javi Palacios</title>
      <link>https://dev.to/fj_palacios</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/fj_palacios"/>
    <language>en</language>
    <item>
      <title>What is JavaScript and Why Node.js Exists</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Fri, 13 Nov 2026 08:28:25 +0000</pubDate>
      <link>https://dev.to/fj_palacios/what-is-javascript-and-why-nodejs-exists-4196</link>
      <guid>https://dev.to/fj_palacios/what-is-javascript-and-why-nodejs-exists-4196</guid>
      <description>&lt;p&gt;JavaScript has a reputation problem. It's a toy language. It has no types. Equality is broken. It lives in the browser getting in the way of designers. All of that is partly true, and none of it has anything to do with what you're going to do in this course.&lt;/p&gt;

&lt;p&gt;Because the same language people complain about online is behind half the APIs you hit every day. Not by accident, either: it got there through a very specific design decision made a while back.&lt;/p&gt;

&lt;p&gt;No JavaScript today. Instead you're going to understand where it came from, why somebody took it out of the browser and put it on a server, and what's actually inside Node when you type &lt;code&gt;node&lt;/code&gt; and press Enter. Everything else in this course stands on that, and a crooked foundation just means you memorise the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  JavaScript was born in the browser
&lt;/h2&gt;

&lt;p&gt;It's 1995. Netscape owns the web and has a problem: pages are static documents. Any form validation means a round trip to the server and back. Brendan Eich writes a language for the browser in a handful of days, in a hurry, with no time to pick a nice design.&lt;/p&gt;

&lt;p&gt;The name was marketing. Java was hot, so the language got called JavaScript despite having nothing to do with Java. It's the part of its history that makes more sense in a sales meeting than in an engineering book.&lt;/p&gt;

&lt;p&gt;Then came standardisation: the language is &lt;strong&gt;ECMAScript&lt;/strong&gt; on paper, and JavaScript is the commercial name of the implementation. In practice everyone says JavaScript, in the same way everyone says aspirin instead of acetylsalicylic acid.&lt;/p&gt;

&lt;p&gt;The interesting bit isn't the trivia, it's the consequence. JavaScript was designed for one environment — the browser — where the main job is &lt;strong&gt;reacting to events&lt;/strong&gt;: a click, a network response, without freezing the interface. That event model wasn't a detail, it was the whole point of the language. Years later somebody looked at it and thought it could do something bigger than a button.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why anyone took it out of there
&lt;/h2&gt;

&lt;p&gt;In 2009 Ryan Dahl introduced Node.js. The problem he was chasing wasn't a JavaScript problem, it was a server problem.&lt;/p&gt;

&lt;p&gt;The classic way to handle connections is one thread per connection: request arrives, thread reserved, wait for the database, respond, release. It works, but every connection costs memory and every thread switch costs time. With thousands of concurrent connections — most of them waiting on a disk or a network — the server spends its day arranging threads that have nothing to do.&lt;/p&gt;

&lt;p&gt;Node proposes the opposite: &lt;strong&gt;one thread for your code&lt;/strong&gt;, and no waiting around. When something needs a file or a socket, Node hands off the wait and moves on. When the answer arrives, it comes back.&lt;/p&gt;

&lt;p&gt;JavaScript was a perfect fit precisely because of its origin story: the browser had already taught it to live on events and callbacks. The language that looked bolted to a browser window turned out to be the one that already knew how not to block.&lt;/p&gt;

&lt;h2&gt;
  
  
  V8 is not Node
&lt;/h2&gt;

&lt;p&gt;Two things get mixed up constantly, and it's worth separating them on day one because half the internet uses them as synonyms.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;V8&lt;/strong&gt; is the JavaScript engine: the program that reads your code, compiles it, and runs it. Google builds it, Chrome ships it, and it knows nothing about files, networks, or servers. It only knows how to run JavaScript very fast.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node.js&lt;/strong&gt; is the whole environment: the engine, plus an I/O library, plus a set of modules for talking to the operating system.&lt;/p&gt;

&lt;p&gt;The usual analogy is an engine and a car. V8 is the engine; Node is the car — chassis, gearbox, fuel tank. An engine on a stand makes a very convincing noise and gets you nowhere.&lt;/p&gt;

&lt;p&gt;Don't take my word for it. Install Node as shown below and ask it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node -p &lt;span class="s2"&gt;"process.versions.v8"&lt;/span&gt;
node -p &lt;span class="s2"&gt;"process.versions.uv"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;14.6.202.34-node.40
1.52.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two numbers, two different parts. The first is V8, the engine. The second is &lt;strong&gt;libuv&lt;/strong&gt;, which deserves its own section.&lt;/p&gt;

&lt;h2&gt;
  
  
  libuv: the one that waits for you
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;libuv&lt;/strong&gt; is what makes Node's promise possible. Its job is talking to the operating system so your process can keep hundreds of operations in flight without hundreds of threads.&lt;/p&gt;

&lt;p&gt;Every OS has its own mechanism: &lt;code&gt;epoll&lt;/code&gt; on Linux, &lt;code&gt;kqueue&lt;/code&gt; on macOS and the BSDs, completion ports on Windows. libuv puts one interface in front of all of them, and adds a &lt;strong&gt;thread pool&lt;/strong&gt; for the jobs the OS can't background on its own — some disk operations, DNS, cryptography.&lt;/p&gt;

&lt;p&gt;That kills a myth worth killing early: Node is not "single-threaded" in the sense that the process has one thread. Your JavaScript runs on one thread, yes, but there is more staff downstairs. What you don't get is one thread per request.&lt;/p&gt;

&lt;p&gt;The practical consequence is the one that matters: while libuv waits, your main thread carries on. It doesn't sit around watching the data arrive; it handles the next thing. And when the data lands, it comes back.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Node wins and what it loses
&lt;/h2&gt;

&lt;p&gt;Node isn't better. It's different, and the difference fits in a table:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Situation&lt;/th&gt;
&lt;th&gt;Node&lt;/th&gt;
&lt;th&gt;The classic alternative&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Many connections waiting on data&lt;/td&gt;
&lt;td&gt;Shines: one thread serves them all&lt;/td&gt;
&lt;td&gt;Every connection costs a thread&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;I/O-heavy API&lt;/td&gt;
&lt;td&gt;Fits naturally&lt;/td&gt;
&lt;td&gt;Works, with more machinery&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real time: chat, notifications&lt;/td&gt;
&lt;td&gt;Open connections are cheap&lt;/td&gt;
&lt;td&gt;Open connections are expensive&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU-heavy work&lt;/td&gt;
&lt;td&gt;Struggles: it blocks the thread&lt;/td&gt;
&lt;td&gt;Spreads the work across threads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Number crunching, heavy processing&lt;/td&gt;
&lt;td&gt;Not its turf&lt;/td&gt;
&lt;td&gt;Exactly its turf&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The row that decides the rest of this course is the fourth. In Node, if your code spends twenty seconds computing, the server stops answering for twenty seconds. There's no spare thread, nobody else at the door. We'll come back to that in detail, and it has a fix — but the fix is never "hope it doesn't happen".&lt;/p&gt;

&lt;h2&gt;
  
  
  JavaScript versus Python
&lt;/h2&gt;

&lt;p&gt;You come from another language, so this comparison isn't decoration: it's the fastest way to learn. These are the differences that actually change how you write code.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Concept&lt;/th&gt;
&lt;th&gt;Python&lt;/th&gt;
&lt;th&gt;JavaScript&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Typing&lt;/td&gt;
&lt;td&gt;Dynamic, with optional type hints&lt;/td&gt;
&lt;td&gt;Dynamic, no types in the language&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Objects&lt;/td&gt;
&lt;td&gt;Classic classes&lt;/td&gt;
&lt;td&gt;Prototypes, with &lt;code&gt;class&lt;/code&gt; as syntactic sugar&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Numbers&lt;/td&gt;
&lt;td&gt;Separate integers and floats&lt;/td&gt;
&lt;td&gt;One type: IEEE 754 double precision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Concurrency&lt;/td&gt;
&lt;td&gt;Threads under the GIL, plus &lt;code&gt;asyncio&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;One thread and an event loop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Execution&lt;/td&gt;
&lt;td&gt;Interpreted by CPython&lt;/td&gt;
&lt;td&gt;Compiled on the fly by V8&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two caveats, because a table lies by omission.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Numbers&lt;/strong&gt; are what trips up Python developers most. JavaScript has no integers: every number is a double-precision float. &lt;code&gt;0.1 + 0.2&lt;/code&gt; isn't &lt;code&gt;0.3&lt;/code&gt;, it's &lt;code&gt;0.30000000000000004&lt;/code&gt;, and that's not a bug, that's the type. Big integers lose precision, which is exactly why &lt;code&gt;BigInt&lt;/code&gt; exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concurrency&lt;/strong&gt; isn't about one being worse. The GIL and the event loop are two answers to the same question: how do you stop a program from staring at the ceiling while it waits.&lt;/p&gt;

&lt;p&gt;You'll see the object model up close in the next lesson. For now, just know that &lt;code&gt;class&lt;/code&gt; in JavaScript is not what it looks like: there are no real classes, there's a prototype chain with pleasant syntax on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Node with nvm
&lt;/h2&gt;

&lt;p&gt;You could install Node from the official site and have exactly one version. That works right up until the first project that needs a different one. Then the ritual starts: uninstall, reinstall, break something, roll back.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;nvm&lt;/strong&gt; is a version manager: it installs several at once and lets you switch per project. That switch is declared in a file, &lt;code&gt;.nvmrc&lt;/code&gt;, which you commit with the code. From day one, because day one is when it costs nothing.&lt;/p&gt;

&lt;p&gt;The official install is a script. If you'd rather use your package manager there are options, though mind the second one:&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;# macOS and Linux (Homebrew)&lt;/span&gt;
brew install nvm

&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
&lt;span class="c"&gt;# there is no official nvm package in the repositories:&lt;/span&gt;
&lt;span class="c"&gt;# the documented route is the install script&lt;/span&gt;

&lt;span class="c"&gt;# Arch Linux (AUR)&lt;/span&gt;
paru -S nvm
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The route the project itself recommends:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh &lt;span class="o"&gt;|&lt;/span&gt; bash
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Real output:&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; Downloading nvm from git to '/home/your-user/.nvm'
=&amp;gt; Cloning into '/home/your-user/.nvm'...
=&amp;gt; Compressing and cleaning up git repository
=&amp;gt; Appending nvm source string to /home/your-user/.bashrc
=&amp;gt; Appending bash_completion source string to /home/your-user/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then open a new terminal, or reload your shell file, so the command exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nvm --version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





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

&lt;/div&gt;



&lt;p&gt;Now the useful part. Install the line that gets long-term support today:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nvm install --lts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Installing latest LTS version.
Downloading and installing node v26.13.1...
Computing checksum with sha256sum
Checksums matched!
Now using node v26.13.1 (npm v11.20.0)
Creating default alias: default -&amp;gt; lts/* (-&amp;gt; v26.13.1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;lts/*&lt;/code&gt; is what you want: not a number that expires, but "whatever LTS is current". Check it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node -v
npm -v
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;v26.13.1
11.20.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And pin it as the default, so a fresh terminal doesn't start with the system one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nvm &lt;span class="nb"&gt;alias&lt;/span&gt; default &lt;span class="s1"&gt;'lts/*'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;default -&amp;gt; lts/* (-&amp;gt; v26.13.1)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a specific project, a &lt;code&gt;.nvmrc&lt;/code&gt; with one line:&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;echo&lt;/span&gt; &lt;span class="s1"&gt;'lts/*'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .nvmrc
nvm use
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Found '/home/your-user/project/.nvmrc' with version &amp;lt;lts/*&amp;gt;
Now using node v26.13.1 (npm v11.20.0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That file gets committed. Anyone who clones the repo runs &lt;code&gt;nvm use&lt;/code&gt; and gets the same version you have, without reading a stale README. It's the "works on my machine" that actually works.&lt;/p&gt;

&lt;h2&gt;
  
  
  First contact with the runtime
&lt;/h2&gt;

&lt;p&gt;Before writing real JavaScript, let's confirm the runtime is where you think it is. Create the course project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkdir node-backend &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd&lt;/span&gt; node-backend
npm init -y
mkdir -p src/lessons
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;code&gt;package.json&lt;/code&gt; and add the line that turns on modern modules:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"module"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;"type": "module"&lt;/code&gt; is not decoration: it changes how Node reads &lt;code&gt;import&lt;/code&gt; and &lt;code&gt;export&lt;/code&gt;, and you'll use it for the whole course. Without it, Node assumes the old system and examples fail in a way that tells you nothing.&lt;/p&gt;

&lt;p&gt;Now the first file, &lt;code&gt;src/lessons/01-runtime.mjs&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Node"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Platform"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;arch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"V8"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;v8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nb"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"libuv"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;versions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uv&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node src/lessons/01-runtime.mjs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Node v26.13.1
Platform linux x64
V8 14.6.202.34-node.40
libuv 1.52.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On your machine &lt;code&gt;linux x64&lt;/code&gt; will read &lt;code&gt;darwin arm64&lt;/code&gt; on a recent Mac, &lt;code&gt;win32 x64&lt;/code&gt; on Windows. The V8 and libuv numbers move with patch releases. What doesn't change is what you just saw: Node isn't a language, it's a program made of parts, and those parts have versions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;process&lt;/code&gt; is the object representing the running process. It's global, always there, and it's the front door to nearly everything a server does: environment variables, arguments, signals, output. You don't need to memorise it today, just know it exists and that it isn't magic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common problems
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;nvm: command not found&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;The install script appends a few lines to &lt;code&gt;.bashrc&lt;/code&gt; or &lt;code&gt;.zshrc&lt;/code&gt;, but it doesn't reload the terminal you already had open. Open a new one, or source the file:&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;source&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On zsh, &lt;code&gt;source ~/.zshrc&lt;/code&gt;. And if you installed nvm through Homebrew, the Homebrew message may tell you to create &lt;code&gt;~/.nvm&lt;/code&gt; first — read it, it says so.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;node -v&lt;/code&gt; shows a different version
&lt;/h3&gt;

&lt;p&gt;Blame &lt;code&gt;PATH&lt;/code&gt;. If Node is also installed by the system, by Homebrew, or by a manager like &lt;code&gt;asdf&lt;/code&gt;, it can come first. Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;which node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the path isn't under &lt;code&gt;~/.nvm/versions/node/…&lt;/code&gt;, that's your problem. Sourcing nvm, opening a new terminal, or running &lt;code&gt;nvm use&lt;/code&gt; in the project fixes it.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;.nvmrc&lt;/code&gt; exists but the version doesn't change
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;.nvmrc&lt;/code&gt; is a declaration, not a command. Someone has to read it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nvm use
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Some shell setups load it automatically when you enter the directory, most don't. If it doesn't switch, run &lt;code&gt;nvm use&lt;/code&gt; by hand before you start working.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;nvm install --lts&lt;/code&gt; fails with a checksum or network error
&lt;/h3&gt;

&lt;p&gt;Usually a corporate network, a proxy, or an odd mirror. Retry, and if it persists, check whether a proxy is intercepting HTTPS. It's not Node and it's not your code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you take from this lesson
&lt;/h2&gt;

&lt;p&gt;You now know the ground you'll be working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JavaScript was built for the browser, and its event model turned out to be useful on a server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;V8&lt;/strong&gt; runs the language; &lt;strong&gt;Node&lt;/strong&gt; is the environment around it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;libuv&lt;/strong&gt; does the waiting, which is why Node handles thousands of connections on one JavaScript thread.&lt;/li&gt;
&lt;li&gt;Node wins at I/O and loses at CPU work. That limit will show up again and again.&lt;/li&gt;
&lt;li&gt;You have Node installed through &lt;strong&gt;nvm&lt;/strong&gt;, with the version pinned in &lt;code&gt;.nvmrc&lt;/code&gt; and an LTS default.&lt;/li&gt;
&lt;li&gt;You ran code in the runtime and saw its parts report their own versions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you come from another language and this feels like translation, that's exactly what it is. Learning Node isn't memorising new syntax: it's translating what you know and finding the three or four places where the translation isn't exact.&lt;/p&gt;

&lt;p&gt;If you don't program yet, this isn't your entry point: the Python from scratch course gives you the foundation this course assumes.&lt;/p&gt;

&lt;p&gt;Next lesson we start on the language itself: variables, functions, destructuring, spread. No explaining what a variable is, and Python sitting next to it the whole way.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>node</category>
      <category>javascript</category>
      <category>backend</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Setting Up PostgreSQL with Docker, psql, and pgAdmin</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Wed, 11 Nov 2026 12:30:36 +0000</pubDate>
      <link>https://dev.to/fj_palacios/setting-up-postgresql-with-docker-psql-and-pgadmin-496j</link>
      <guid>https://dev.to/fj_palacios/setting-up-postgresql-with-docker-psql-and-pgadmin-496j</guid>
      <description>&lt;p&gt;Time for the pleasant part: stop talking about databases as an idea and run one for real.&lt;/p&gt;

&lt;p&gt;You open the terminal, type &lt;code&gt;docker compose up&lt;/code&gt;, logs start flying, one line says &lt;code&gt;database system is ready to accept connections&lt;/code&gt;, another looks like it was written by a printer having a difficult morning, and now you're thinking: is that it? Is PostgreSQL running? Where do I connect? Which user? Why does a database need more environment variables than some projects have tests?&lt;/p&gt;

&lt;p&gt;Relax. We're not deploying production. We're building a lab. And a good lab needs three things: a database you can break without apologizing, a fast way to talk to it from the terminal, and a visual interface for looking around without feeling like you're defusing a bomb.&lt;/p&gt;

&lt;p&gt;Today we'll set up that lab with &lt;strong&gt;PostgreSQL 18&lt;/strong&gt;, &lt;strong&gt;Docker Compose&lt;/strong&gt;, &lt;strong&gt;psql&lt;/strong&gt;, and &lt;strong&gt;pgAdmin&lt;/strong&gt;. CLI first, because &lt;code&gt;psql&lt;/code&gt; is where you'll really learn SQL. pgAdmin comes second as a visual helper, for those days when a tree of tables feels calmer than another blinking prompt. We all have those days.&lt;/p&gt;

&lt;p&gt;One thing before we go on: this assumes you have Docker installed and know how to bring something up with Compose. If that is not you yet, the Docker Compose lesson is where to start.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we are actually running
&lt;/h2&gt;

&lt;p&gt;Our environment will have two containers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;postgres&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;postgres:18&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The PostgreSQL server where the data lives&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pgadmin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;dpage/pgadmin4:9.15&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;A web UI for connecting to PostgreSQL&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We'll also use a Docker &lt;strong&gt;volume&lt;/strong&gt; to persist PostgreSQL data. That matters: if you remove the container but keep the volume, your data stays. If you remove the volume, it disappears. Docker has no emotional attachment to your tables. Useful to remember before experimenting with commands that start with &lt;code&gt;docker volume rm&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The shape is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Your terminal
     │
     ├── docker compose
     │       ├── postgres container → PostgreSQL server
     │       └── pgadmin container  → Web UI
     │
     └── psql → command-line client
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the &lt;a href="https://dev.to/en/courses/mastering-docker-from-scratch"&gt;Docker course&lt;/a&gt;, we already work with containers, ports, and volumes. Here we won't become Docker administrators again; we'll use it as a tool to get PostgreSQL clean, reproducible, and easy to restart after we do something educational. “Educational” sometimes means “I broke everything.” That's part of the process.&lt;/p&gt;

&lt;p&gt;Let's create the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the lab project
&lt;/h2&gt;

&lt;p&gt;Create a folder for the course:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkdir -p ~/sql-course
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/sql-course
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside it, create a file named &lt;code&gt;compose.yaml&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;touch compose.yaml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now write this configuration:&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="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:18&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sql-course-postgres&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;course_user&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;course_password&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;course_db&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"5432:5432"&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CMD-SHELL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"pg_isready -U course_user -d course_db"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;

  &lt;span class="na"&gt;pgadmin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dpage/pgadmin4:9.15&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;sql-course-pgadmin&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;PGADMIN_DEFAULT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin@example.com&lt;/span&gt;
      &lt;span class="na"&gt;PGADMIN_DEFAULT_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin_password&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"5050:80"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
      &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;pgadmin_data:/var/lib/pgadmin&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pgadmin_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before running it, let's understand it. Copying YAML without understanding it is like signing a contract because “the indentation looks clean.” Dangerous, though aesthetically pleasing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding the &lt;code&gt;compose.yaml&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;The first part defines the &lt;code&gt;postgres&lt;/code&gt; service:&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;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:18&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tells Docker to use the official PostgreSQL 18 image. We could use &lt;code&gt;latest&lt;/code&gt;, but for a course that's a bad idea. &lt;code&gt;latest&lt;/code&gt; can mean one thing today and another thing tomorrow. And when you're learning, having the floor move under your feet is not helpful. You already have &lt;code&gt;NULL&lt;/code&gt; waiting for its turn.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;container_name&lt;/code&gt; line below is optional: it pins the container name so &lt;code&gt;docker ps&lt;/code&gt; and &lt;code&gt;docker compose logs postgres&lt;/code&gt; stay predictable instead of growing a suffix. Production files usually leave it out, because it stops the service from scaling to several replicas; in a lab, stable names are worth more than that freedom.&lt;/p&gt;

&lt;p&gt;Then we have the environment variables:&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;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;course_user&lt;/span&gt;
  &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;course_password&lt;/span&gt;
  &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;course_db&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The official PostgreSQL image reads these variables when it initializes an empty database:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POSTGRES_USER&lt;/code&gt;: creates the initial user.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt;: sets that user's password.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;POSTGRES_DB&lt;/code&gt;: creates the initial database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This only applies &lt;strong&gt;the first time the volume is initialized&lt;/strong&gt;. If you change &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt; after the container has already run, PostgreSQL won't magically rewrite the password. The data already exists inside the volume. Docker doesn't read your mind, and honestly that's probably for the best.&lt;/p&gt;

&lt;p&gt;Then we expose the port:&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;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"5432:5432"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first &lt;code&gt;5432&lt;/code&gt; is the port on your machine. The second &lt;code&gt;5432&lt;/code&gt; is the port inside the container. PostgreSQL listens on &lt;code&gt;5432&lt;/code&gt; by default, so we keep the classic mapping.&lt;/p&gt;

&lt;p&gt;Next comes a very important line:&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;volumes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres_data:/var/lib/postgresql&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For PostgreSQL 18, the official image uses &lt;code&gt;/var/lib/postgresql&lt;/code&gt; as the recommended volume location. The actual data directory lives inside that structure. Could you mount a different path? Yes. Do you need to today? No. Simplicity first. There will be time later for backups, upgrades, and decisions that make a DBA stare silently out the window.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;healthcheck&lt;/code&gt; asks whether PostgreSQL is ready:&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;healthcheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"CMD-SHELL"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"pg_isready -U course_user -d course_db"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;pg_isready&lt;/code&gt; asks the server whether it is accepting connections and returns an exit code based on the answer. &lt;code&gt;-U&lt;/code&gt; and &lt;code&gt;-d&lt;/code&gt; tell it which user and which database to ask for; without them it would use the container's default user, which is not the one you want.&lt;/p&gt;

&lt;p&gt;Careful with the attribution, because it is the most common misreading of a Compose file: &lt;strong&gt;the healthcheck does not wait for anything&lt;/strong&gt;. It only declares the service's state, moving from &lt;code&gt;starting&lt;/code&gt; to &lt;code&gt;healthy&lt;/code&gt; once the first check passes. What actually waits is the condition on the other service, and that comes a couple of blocks further down.&lt;/p&gt;

&lt;p&gt;The second service defines pgAdmin:&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;pgadmin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dpage/pgadmin4:9.15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;pgAdmin needs an email and password to create its initial administrator user:&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;environment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;PGADMIN_DEFAULT_EMAIL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin@example.com&lt;/span&gt;
  &lt;span class="na"&gt;PGADMIN_DEFAULT_PASSWORD&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;admin_password&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a local lab. Do not use these credentials in production. That sounds obvious, but uploading &lt;code&gt;.env&lt;/code&gt; files to GitHub also sounded obvious, and yet the internet is what it is.&lt;/p&gt;

&lt;p&gt;Finally, we expose pgAdmin on port &lt;code&gt;5050&lt;/code&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="na"&gt;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"5050:80"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means you'll open pgAdmin at &lt;code&gt;http://localhost:5050&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And finally, the line that ties the two services together:&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;depends_on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is what actually keeps pgAdmin from starting too early: Docker waits for the &lt;code&gt;postgres&lt;/code&gt; healthcheck to report &lt;code&gt;healthy&lt;/code&gt; and only then brings &lt;code&gt;pgadmin&lt;/code&gt; up. Without the healthcheck that condition would have nothing to check; without the condition, the healthcheck would report and nobody would listen. Both pieces together are what remove the classic “I opened the UI and it says it can't connect”.&lt;/p&gt;

&lt;p&gt;We have the blueprint. Now let's power it on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Starting PostgreSQL and pgAdmin
&lt;/h2&gt;

&lt;p&gt;From the &lt;code&gt;~/sql-course&lt;/code&gt; folder, run:&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 up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-d&lt;/code&gt; flag starts everything in the background. Without &lt;code&gt;-d&lt;/code&gt;, the terminal stays attached to the logs, which is sometimes useful and sometimes like watching a fireplace made of text.&lt;/p&gt;

&lt;p&gt;Check that the services are running:&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 ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see something 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;NAME                  IMAGE                 STATUS                    PORTS
sql-course-postgres   postgres:18           Up (healthy)              0.0.0.0:5432-&amp;gt;5432/tcp
sql-course-pgadmin    dpage/pgadmin4:9.15   Up                        0.0.0.0:5050-&amp;gt;80/tcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;postgres&lt;/code&gt; says &lt;code&gt;starting&lt;/code&gt;, wait a few seconds and run the command again. Databases also deserve to start with dignity.&lt;/p&gt;

&lt;p&gt;You can inspect the logs with:&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 postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The line you're looking for is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;database system is ready to accept connections
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once you see that, PostgreSQL is ready.&lt;/p&gt;

&lt;p&gt;Now let's talk to it from the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting with &lt;code&gt;psql&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;psql&lt;/code&gt; is PostgreSQL's command-line client. It's direct, powerful, and at first a little dry, like many tools you eventually learn to respect.&lt;/p&gt;

&lt;p&gt;You can enter &lt;code&gt;psql&lt;/code&gt; inside the container:&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 &lt;span class="nb"&gt;exec&lt;/span&gt; postgres psql -U course_user -d course_db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If everything works, you'll see something 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;psql (18.4 (Debian 18.4-1.pgdg13+2))
Type "help" for help.

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

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;course_db=#&lt;/code&gt; prompt means you're inside PostgreSQL, connected to the &lt;code&gt;course_db&lt;/code&gt; database.&lt;/p&gt;

&lt;p&gt;Try your first 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="nf"&gt;version&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see something 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;                                              version
-----------------------------------------------------------------------------------------------------
 PostgreSQL 18.4 (Debian 18.4-1.pgdg13+2) on x86_64-pc-linux-gnu, compiled by gcc 14.2.0, 64-bit
(1 row)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then try something smaller:&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="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; result
--------
      2
(1 row)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;AS result&lt;/code&gt;. That's an &lt;strong&gt;alias&lt;/strong&gt;: a temporary name for the output column. In this course we'll use aliases in &lt;code&gt;snake_case&lt;/code&gt;, descriptive, and without quotes unless there is a strong reason. That means &lt;code&gt;total_spent&lt;/code&gt;, not &lt;code&gt;Total Spent&lt;/code&gt;, not &lt;code&gt;x&lt;/code&gt;, not &lt;code&gt;prettyColumnBecauseReasons&lt;/code&gt;. Queries deserve names that don't look like they were invented at 3 AM.&lt;/p&gt;

&lt;p&gt;To exit &lt;code&gt;psql&lt;/code&gt;, type:&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="n"&gt;q&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, it starts with a backslash. In &lt;code&gt;psql&lt;/code&gt;, client-specific commands use &lt;code&gt;\&lt;/code&gt;. SQL queries end with &lt;code&gt;;&lt;/code&gt;. At first this looks like a tiny oddity. Then you discover that &lt;code&gt;psql&lt;/code&gt; has a whole universe of internal commands and accept that terminals also have secret rooms.&lt;/p&gt;

&lt;p&gt;Let's look at the useful ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic &lt;code&gt;psql&lt;/code&gt; commands
&lt;/h2&gt;

&lt;p&gt;Enter again:&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 &lt;span class="nb"&gt;exec&lt;/span&gt; postgres psql -U course_user -d course_db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;psql&lt;/code&gt;, try:&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="n"&gt;l&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lists databases. You'll see four: yours, &lt;code&gt;postgres&lt;/code&gt;, and the two templates PostgreSQL uses to create new databases.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                                       List of databases
   Name    |   Owner     | Encoding | Locale Provider |  Collate   |   Ctype
-----------+-------------+----------+-----------------+------------+------------
 course_db | course_user | UTF8     | libc            | en_US.utf8 | en_US.utf8
 postgres  | course_user | UTF8     | libc            | en_US.utf8 | en_US.utf8
 template0 | course_user | UTF8     | libc            | en_US.utf8 | en_US.utf8
 template1 | course_user | UTF8     | libc            | en_US.utf8 | en_US.utf8
(4 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ICU and privilege columns are trimmed here; PostgreSQL 18 prints more of them. What matters is that you recognise the shape.&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="n"&gt;conninfo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shows which database, user, and server you're connected to.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are connected to database "course_db" as user "course_user" via socket in "/var/run/postgresql" at port "5432".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;/var/run/postgresql&lt;/code&gt; is the tell that you are inside the container, talking over the local socket instead of coming in over the network.&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="n"&gt;dt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Lists tables in the current schema. Right now you'll probably see none, because we haven't created any tables yet. That's fine. A fresh lab doesn't have samples on the table.&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="o"&gt;?&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Shows help for &lt;code&gt;psql&lt;/code&gt; commands. It's long. Very long. Don't try to memorize it. Nobody becomes a better person by learning every meta-command at once.&lt;/p&gt;

&lt;p&gt;For now, these are the ones you need:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\q&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Exit &lt;code&gt;psql&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\l&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List databases&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\conninfo&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show current connection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\dt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List tables&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\d table_name&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Describe a table&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;\?&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show &lt;code&gt;psql&lt;/code&gt; help&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And here's the key difference:&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is SQL.&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="n"&gt;dt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is a &lt;code&gt;psql&lt;/code&gt; command.&lt;/p&gt;

&lt;p&gt;If you mix them up and something fails, it doesn't mean you're bad at SQL. It means you're learning where the language ends and the tool begins. Small border, large confusion. Classic computing.&lt;/p&gt;

&lt;p&gt;Now let's open pgAdmin.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connecting with pgAdmin
&lt;/h2&gt;

&lt;p&gt;Open your browser at:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;http://localhost:5050
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Log in with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Email: admin@example.com
Password: admin_password
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see pgAdmin. Breathe. Yes, it has many panels. Yes, it looks like an airplane cockpit designed by someone who deeply loves context menus. You don't need to understand all of it today.&lt;/p&gt;

&lt;p&gt;To register the PostgreSQL server:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Right-click &lt;strong&gt;Servers&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Choose &lt;strong&gt;Register&lt;/strong&gt; → &lt;strong&gt;Server...&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;In the &lt;strong&gt;General&lt;/strong&gt; tab, use this name:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SQL Course PostgreSQL
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;In the &lt;strong&gt;Connection&lt;/strong&gt; tab, use these values:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&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;Host name/address&lt;/td&gt;
&lt;td&gt;&lt;code&gt;postgres&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Port&lt;/td&gt;
&lt;td&gt;&lt;code&gt;5432&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Maintenance database&lt;/td&gt;
&lt;td&gt;&lt;code&gt;course_db&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Username&lt;/td&gt;
&lt;td&gt;&lt;code&gt;course_user&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Password&lt;/td&gt;
&lt;td&gt;&lt;code&gt;course_password&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The important detail is the host: &lt;strong&gt;inside Docker Compose, the service is named &lt;code&gt;postgres&lt;/code&gt;&lt;/strong&gt;. pgAdmin should not connect to &lt;code&gt;localhost&lt;/code&gt;, because from pgAdmin's point of view &lt;code&gt;localhost&lt;/code&gt; means “the pgAdmin container itself”, not your machine and not the PostgreSQL container.&lt;/p&gt;

&lt;p&gt;This breaks brains. Not because it's advanced, but because the word &lt;code&gt;localhost&lt;/code&gt; looks innocent and then turns out to have contextual identity. Like a polite villain.&lt;/p&gt;

&lt;p&gt;Save the connection. If everything works, you'll see the server in the left tree. Open &lt;code&gt;Databases&lt;/code&gt; and you should see &lt;code&gt;course_db&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;pgAdmin will help you inspect structures, explore tables, and run queries visually. But we'll build the muscle in &lt;code&gt;psql&lt;/code&gt;. The UI helps; the terminal teaches. Also, no Electron fan heroics, which is always a small act of mercy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating a test table
&lt;/h2&gt;

&lt;p&gt;Back to &lt;code&gt;psql&lt;/code&gt; for a real check:&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 &lt;span class="nb"&gt;exec&lt;/span&gt; postgres psql -U course_user -d course_db
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a simple table:&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;books&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="kt"&gt;INTEGER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="kt"&gt;TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;author&lt;/span&gt; &lt;span class="kt"&gt;TEXT&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now list the tables:&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="n"&gt;dt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You should see something 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;         List of relations
 Schema | Name  | Type  |    Owner
--------+-------+-------+-------------
 public | books | table | course_user
(1 row)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you want to see what is inside that table without opening pgAdmin:&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="n"&gt;d&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                        Table "public.books"
 Column |  Type   | Collation | Nullable | Default
--------+---------+-----------+----------+---------
 id     | integer |           |          |
 title  | text    |           |          |
 author | text    |           |          |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Columns and types, still without &lt;code&gt;PRIMARY KEY&lt;/code&gt; or &lt;code&gt;NOT NULL&lt;/code&gt;. That is deliberate: they arrive in their own lesson.&lt;/p&gt;

&lt;p&gt;Insert a couple of rows:&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;INTO&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;&lt;span class="p"&gt;)&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;'The Pragmatic Programmer'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Andrew Hunt and David Thomas'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&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;'Designing Data-Intensive Applications'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'Martin Kleppmann'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Query the 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;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;author&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;books&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; id |                title                 |             author
----+--------------------------------------+--------------------------------
  1 | The Pragmatic Programmer             | Andrew Hunt and David Thomas
  2 | Designing Data-Intensive Applications | Martin Kleppmann
(2 rows)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You just created a table, inserted data, and queried it. We haven't discussed primary keys, detailed data types, constraints, or design yet. That's coming soon. Right now the important part is proving that the lab works.&lt;/p&gt;

&lt;p&gt;If you open pgAdmin and refresh &lt;code&gt;Schemas&lt;/code&gt; → &lt;code&gt;public&lt;/code&gt; → &lt;code&gt;Tables&lt;/code&gt;, you should see the &lt;code&gt;books&lt;/code&gt; table. If it doesn't appear, right-click and choose &lt;strong&gt;Refresh&lt;/strong&gt;. pgAdmin sometimes needs to be reminded that the world has changed. We all know someone like that.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stopping, starting, and cleaning the environment
&lt;/h2&gt;

&lt;p&gt;To stop the containers without deleting data:&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 down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To start them again:&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 up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your &lt;code&gt;books&lt;/code&gt; table will still be there because it is stored in the &lt;code&gt;postgres_data&lt;/code&gt; volume.&lt;/p&gt;

&lt;p&gt;Check it:&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 &lt;span class="nb"&gt;exec&lt;/span&gt; postgres psql -U course_user -d course_db -c &lt;span class="s2"&gt;"SELECT id, title FROM books;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-c&lt;/code&gt; flag runs a query and exits. Very useful for quick checks.&lt;/p&gt;

&lt;p&gt;If you want to delete the whole lab, including data:&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 down -v
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ That &lt;code&gt;-v&lt;/code&gt; removes the volumes defined by the Compose file. Here that means deleting &lt;code&gt;postgres_data&lt;/code&gt; and &lt;code&gt;pgadmin_data&lt;/code&gt;. In a lab, that's fine. In a real environment, this command can turn a normal morning into a meeting with security, product, and someone asking “do we have a backup?”&lt;/p&gt;

&lt;p&gt;Use &lt;code&gt;down -v&lt;/code&gt; only when you really want to start from zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common problems
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Port &lt;code&gt;5432&lt;/code&gt; is already in use
&lt;/h3&gt;

&lt;p&gt;If you have PostgreSQL installed locally, port &lt;code&gt;5432&lt;/code&gt; may already be taken.&lt;/p&gt;

&lt;p&gt;Change the external port:&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;ports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
  &lt;span class="p"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"5433:5432"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside Docker it remains &lt;code&gt;5432&lt;/code&gt;; from your machine you connect through &lt;code&gt;5433&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  pgAdmin does not connect with &lt;code&gt;localhost&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Inside pgAdmin, use &lt;code&gt;postgres&lt;/code&gt; as the host, not &lt;code&gt;localhost&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;localhost&lt;/code&gt; from a container means “myself.” And pgAdmin, despite its enthusiasm, is not PostgreSQL.&lt;/p&gt;

&lt;h3&gt;
  
  
  I changed the password and it still fails
&lt;/h3&gt;

&lt;p&gt;If the volume already existed, changing &lt;code&gt;POSTGRES_PASSWORD&lt;/code&gt; in &lt;code&gt;compose.yaml&lt;/code&gt; does not change the password inside PostgreSQL.&lt;/p&gt;

&lt;p&gt;To restart from scratch:&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 down -v
docker compose up -d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This deletes data. Repeating it because this is where someone scrolls too fast and then looks at the terminal like it betrayed them: &lt;strong&gt;this deletes data&lt;/strong&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;psql&lt;/code&gt; is waiting because I forgot the semicolon
&lt;/h3&gt;

&lt;p&gt;In SQL, many statements end with &lt;code&gt;;&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If you write:&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="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;psql&lt;/code&gt; will show 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;course_db-#
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It isn't broken. It is waiting for you to finish the statement. Type:&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="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And it will continue. The first time it's scary. The second time it's annoying. The third time it's just who you are.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you take from this lesson
&lt;/h2&gt;

&lt;p&gt;Today you set up the base environment for the course:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PostgreSQL 18 running in Docker.&lt;/li&gt;
&lt;li&gt;A persistent volume so recreating containers doesn't delete your data.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;psql&lt;/code&gt; as the main tool for running SQL.&lt;/li&gt;
&lt;li&gt;pgAdmin as a visual helper.&lt;/li&gt;
&lt;li&gt;A first &lt;code&gt;books&lt;/code&gt; table to prove everything works.&lt;/li&gt;
&lt;li&gt;The difference between SQL commands and &lt;code&gt;psql&lt;/code&gt; meta-commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is not “installing a tool.” This is preparing the workbench. From now on, when we talk about tables, columns, rows, constraints, or queries, they won't be drawings in the air: you'll have a real database where you can test things and break them with some elegance.&lt;/p&gt;

&lt;p&gt;In the next lesson we'll write our first &lt;code&gt;SELECT&lt;/code&gt; queries: choosing columns, using aliases, filtering results, and starting to read data with intention. That's where SQL stops being “that thing under the ORM” and starts looking like a very precise way to ask questions.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Linux essentials for DevOps: navigation, files, and permissions</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Tue, 10 Nov 2026 14:20:31 +0000</pubDate>
      <link>https://dev.to/fj_palacios/linux-essentials-for-devops-navigation-files-and-permissions-2fbi</link>
      <guid>https://dev.to/fj_palacios/linux-essentials-for-devops-navigation-files-and-permissions-2fbi</guid>
      <description>&lt;p&gt;You enter a Linux machine for the first time with actual professional intent. Not “I'll paste two commands from Stack Overflow and may the gods of production be kind,” but real professional intent.&lt;/p&gt;

&lt;p&gt;You type &lt;code&gt;ls /&lt;/code&gt; and 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;bin  boot  dev  etc  home  lib  opt  proc  root  tmp  usr  var
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And of course: what is &lt;code&gt;etc&lt;/code&gt;? Why does &lt;code&gt;usr&lt;/code&gt; look like someone lost a vowel? Is &lt;code&gt;var&lt;/code&gt; short for variable? Is &lt;code&gt;root&lt;/code&gt; a folder or a person with too much power? Can you touch anything or will the server start producing administrative smoke?&lt;/p&gt;

&lt;p&gt;Don't worry. Linux is not trying to intimidate you. Well, maybe a little. But beneath those short names there is a solid logic. In DevOps, that logic is not optional: servers live on Linux, logs live on Linux, services are configured on Linux, and many incidents start with someone saying “I only moved a file.” Spoiler: it was never just a file.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Linux filesystem is not one giant folder
&lt;/h2&gt;

&lt;p&gt;In Linux everything hangs from a single root: &lt;code&gt;/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;There is no &lt;code&gt;C:\&lt;/code&gt;, &lt;code&gt;D:\&lt;/code&gt;, or separate drive tree like on Windows. There is one tree. Everything lives inside that tree: disks, users, configuration, logs, processes, devices, and a few corners that look like they were named to check whether you were paying attention.&lt;/p&gt;

&lt;p&gt;A simplified map 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;/
├── bin      → Essential commands like ls, cp, mv
├── etc      → System and service configuration
├── home     → User directories
├── opt      → Optional third-party software
├── tmp      → Temporary files
├── usr      → User-space programs and libraries
└── var      → Variable data: logs, caches, queues
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of &lt;code&gt;/&lt;/code&gt; as a technical building:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/home&lt;/code&gt; contains the users' apartments.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/etc&lt;/code&gt; is the room with manuals, keys, and configuration.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/var&lt;/code&gt; is the storage room where things change constantly: logs, queues, caches.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/tmp&lt;/code&gt; is the workbench where temporary stuff is left and later cleaned up.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/bin&lt;/code&gt; and &lt;code&gt;/usr/bin&lt;/code&gt; are shelves full of tools.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You don't need to memorize every directory today. No serious person learns Linux by reciting &lt;code&gt;/usr/libexec&lt;/code&gt; like medieval poetry. What you need is to know where things usually live and, above all, not treat &lt;code&gt;/&lt;/code&gt; like a downloads folder you can reorganize “your way.”&lt;/p&gt;

&lt;p&gt;Let's move through that tree.&lt;/p&gt;

&lt;h2&gt;
  
  
  Knowing where you are: &lt;code&gt;pwd&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, and &lt;code&gt;cd&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Before touching anything in a terminal, you need orientation. In a graphical interface you see windows, icons, and folders. In a terminal, your map starts with three commands:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;pwd&lt;/code&gt;: prints where you are.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;ls&lt;/code&gt;: lists what is there.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cd&lt;/code&gt;: changes directory.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Enter the course VM if you're following the recommended setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass shell devops-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now ask where you are:&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;pwd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/home/ubuntu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means you are in the personal directory of the &lt;code&gt;ubuntu&lt;/code&gt; user.&lt;/p&gt;

&lt;p&gt;List its content:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;You'll probably see little or nothing. Normal. A newly created home doesn't have boxes all over the floor yet. Give it time.&lt;/p&gt;

&lt;p&gt;Now use more useful formats:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ls -l
ls -lh
ls -la
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference matters:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it shows&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ls&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Basic names&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ls -l&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Long format: permissions, owner, size&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ls -lh&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Human-readable sizes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ls -la&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Hidden files too, like &lt;code&gt;.bashrc&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ls -ltr&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Sorted by date, most recent last&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Files starting with &lt;code&gt;.&lt;/code&gt; are hidden by convention. They are not magic. They are not ashamed. They are usually configuration files you don't want mixed with everything else.&lt;/p&gt;

&lt;p&gt;Move around:&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; /var/log
&lt;span class="nb"&gt;pwd&lt;/span&gt;
ls -lh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go back home:&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; ~
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Go up one level:&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; ..
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Return to the previous directory:&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; -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cd -&lt;/code&gt; looks like a tiny detail until you're jumping between &lt;code&gt;/etc/nginx/sites-available&lt;/code&gt; and &lt;code&gt;/var/log/nginx&lt;/code&gt; and start feeling like you could get lost inside your own terminal. Then it stops being a detail and becomes basic survival.&lt;/p&gt;

&lt;p&gt;You can now look and move. Time to create your own ground.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating directories without starting an archaeological dig
&lt;/h2&gt;

&lt;p&gt;In the previous tutorial we created a workspace called &lt;code&gt;~/devops-course&lt;/code&gt;. Let's use 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="nb"&gt;cd&lt;/span&gt; ~/devops-course
&lt;span class="nb"&gt;pwd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you don't have it, create it now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkdir -p ~/devops-course/&lt;span class="o"&gt;{&lt;/span&gt;scripts,configs,logs,projects,notes&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/devops-course
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-p&lt;/code&gt; flag in &lt;code&gt;mkdir&lt;/code&gt; means: “create parent directories if they don't exist and don't complain if they already do.” It is one of those humble flags that prevents unnecessary drama.&lt;/p&gt;

&lt;p&gt;Compare:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkdir projects/app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;projects&lt;/code&gt; does not exist, it fails.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mkdir -p projects/app/src
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the whole path is missing, it creates it. No speech. No Jira ticket.&lt;/p&gt;

&lt;p&gt;To see the structure more nicely, you can use &lt;code&gt;tree&lt;/code&gt;. If you don't have it installed:&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;# macOS and Linux (Homebrew)&lt;/span&gt;
brew install tree

&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
sudo apt install -y tree

&lt;span class="c"&gt;# Arch Linux&lt;/span&gt;
sudo pacman -S tree
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tree ~/devops-course
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/home/ubuntu/devops-course
├── configs
├── logs
├── notes
├── projects
└── scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tree&lt;/code&gt; is not mandatory, but it helps a lot when a structure grows. And in DevOps, structures grow. Sometimes in healthy ways. Sometimes like a &lt;code&gt;final-FINAL-v2&lt;/code&gt; folder nobody dares to delete.&lt;/p&gt;

&lt;p&gt;You have space. Now let's create files.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating, copying, moving, and renaming files
&lt;/h2&gt;

&lt;p&gt;Create an empty file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;touch notes/lesson-03.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Write one line of text:&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;printf&lt;/span&gt; &lt;span class="s1"&gt;'Linux navigation practice\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; notes/lesson-03.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;&amp;gt;&lt;/code&gt;: it redirects output and &lt;strong&gt;overwrites&lt;/strong&gt; the file. If there was content before, it replaces it. No questions asked. Linux trusts you a lot, which is beautiful until you remember that sometimes you shouldn't trust yourself that much either.&lt;/p&gt;

&lt;p&gt;Append content without deleting what was already there:&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;printf&lt;/span&gt; &lt;span class="s1"&gt;'Current directory: %s\n'&lt;/span&gt; &lt;span class="s2"&gt;"$(pwd)"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; notes/lesson-03.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;&amp;gt;&amp;gt;&lt;/code&gt; appends to the end.&lt;/p&gt;

&lt;p&gt;Now copy the file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cp notes/lesson-03.md notes/lesson-03-copy.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rename it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mv notes/lesson-03-copy.md notes/linux-filesystem-notes.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Move it to another directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;mv notes/linux-filesystem-notes.md configs/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, &lt;code&gt;mv&lt;/code&gt; is used both for moving and renaming. It is the same concept: changing a file path. If the name changes inside the same directory, we call it renaming. If the directory changes, we call it moving. The tool does not need two different buttons to feel complete.&lt;/p&gt;

&lt;p&gt;To copy directories, you need &lt;code&gt;-r&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;cp -r configs configs-backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-r&lt;/code&gt; means recursive: copy the directory and its content. Without &lt;code&gt;-r&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt; refuses because a directory is not a simple file. Good for &lt;code&gt;cp&lt;/code&gt;. Someone had to set boundaries.&lt;/p&gt;

&lt;p&gt;Before deleting anything, let's learn to inspect content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reading files without opening an editor
&lt;/h2&gt;

&lt;p&gt;On servers, you often don't want to edit a file. You just want to see it. For that you have several tools:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cat notes/lesson-03.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;cat&lt;/code&gt; prints the whole file. Good for small files. For large files, &lt;code&gt;cat&lt;/code&gt; is like opening a hose inside your house: everything comes out, technically it works, but maybe that was not the plan.&lt;/p&gt;

&lt;p&gt;See the beginning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;head -n 5 /var/log/syslog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the end:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tail -n 5 /var/log/syslog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow a file in real time:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tail -f /var/log/syslog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;tail -f&lt;/code&gt; is fundamental for logs. It watches the file and prints new lines as they appear. It is the terminal equivalent of standing next to a door waiting for someone to enter, but with more professional value.&lt;/p&gt;

&lt;p&gt;Navigate a long file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;less /var/log/syslog
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;less&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;Space&lt;/code&gt;: move down one page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;b&lt;/code&gt;: move up one page.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/text&lt;/code&gt;: search.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;n&lt;/code&gt;: next match.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;q&lt;/code&gt;: quit.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yes, &lt;code&gt;q&lt;/code&gt; to quit. If that just reminded you of Vim, fair. And if Vim still stares at you from a corner like a final boss, &lt;a href="https://dev.to/en/courses/mastering-vim-from-scratch"&gt;Mastering Vim from Scratch&lt;/a&gt; starts exactly with surviving that first encounter.&lt;/p&gt;

&lt;p&gt;You can now create, move, and read. Next comes the part where slowing down is wise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deleting files: the chainsaw moment
&lt;/h2&gt;

&lt;p&gt;Delete a file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rm configs/linux-filesystem-notes.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Delete an empty directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rmdir configs-backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the directory has content, &lt;code&gt;rmdir&lt;/code&gt; refuses. Good. That refusal is a handrail.&lt;/p&gt;

&lt;p&gt;Delete a directory with everything inside:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rm -r configs-backup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here comes the classic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rm -rf some-directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-r&lt;/code&gt; deletes recursively. &lt;code&gt;-f&lt;/code&gt; forces and reduces questions. Together they are useful. Also dangerous. &lt;code&gt;rm -rf&lt;/code&gt; is not evil, but it is a chainsaw: legitimate tool, terrible idea to use while sleepy and without checking where it points.&lt;/p&gt;

&lt;p&gt;Simple rule:&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;pwd&lt;/span&gt;
ls -la some-directory
rm -rf some-directory
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;First confirm where you are. Then confirm what you are deleting. Then delete.&lt;/p&gt;

&lt;p&gt;If you're writing scripts, use the defensive version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;rm -rf -- &lt;span class="s2"&gt;"$target_directory"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;--&lt;/code&gt; marks the end of options and prevents a weird value starting with &lt;code&gt;-&lt;/code&gt; from being interpreted as a flag. The quotes are what keep spaces and special characters from turning your script into an episode of a true-crime podcast. Both are the groundwork for everything that follows: in the coming lessons you'll meet them inside real scripts, where one unprotected space stops being a detail and becomes a half-deployed service.&lt;/p&gt;

&lt;p&gt;If you want the whole story, the automation part of this course covers it end to end.&lt;/p&gt;

&lt;p&gt;Now let's talk permissions, because sooner or later you'll see &lt;code&gt;Permission denied&lt;/code&gt; and think Linux has something personal against you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding permissions: &lt;code&gt;rwx&lt;/code&gt; without dark magic
&lt;/h2&gt;

&lt;p&gt;Run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ls -l notes/lesson-03.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see something 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;-rw-r--r-- 1 ubuntu ubuntu 62 Aug  3 10:00 notes/lesson-03.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important part is this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;-rw-r--r--
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read it 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;- rw- r-- r--
│ │   │   └── others
│ │   └────── group
│ └────────── owner
└──────────── type
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first character indicates the type:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;-&lt;/code&gt;: regular file.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;d&lt;/code&gt;: directory.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;l&lt;/code&gt;: symbolic link.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then come three blocks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;owner;&lt;/li&gt;
&lt;li&gt;group;&lt;/li&gt;
&lt;li&gt;others.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each block can contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;r&lt;/code&gt;: read;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;w&lt;/code&gt;: write;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;x&lt;/code&gt;: execute.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a file, &lt;code&gt;x&lt;/code&gt; means it can be executed as a program or script. For a directory, &lt;code&gt;x&lt;/code&gt; means it can be entered or traversed. Yes, the same symbol changes meaning slightly depending on context. Linux does these things. Don't stress: with practice it starts making sense.&lt;/p&gt;

&lt;p&gt;Create a small script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cat &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; scripts/hello.sh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s1"&gt;'EOF'&lt;/span&gt;
&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'Hello from Linux\n'&lt;/span&gt;
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Try to run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/hello.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll probably get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bash: ./scripts/hello.sh: Permission denied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The file exists, but it has no execute permission. Add it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chmod +x scripts/hello.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./scripts/hello.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello from Linux
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll also see permissions in numeric form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chmod 755 scripts/hello.sh
chmod 644 notes/lesson-03.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Practical translation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mode&lt;/th&gt;
&lt;th&gt;Usual meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;755&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;owner everything, others read and execute&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;644&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;owner reads/writes, others only read&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;600&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;only the owner reads/writes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;700&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;only the owner has everything&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Don't memorize every number like safe combinations. Keep the common ones. In DevOps you'll see &lt;code&gt;600&lt;/code&gt; for private SSH keys, &lt;code&gt;700&lt;/code&gt; for &lt;code&gt;.ssh&lt;/code&gt;, &lt;code&gt;755&lt;/code&gt; for executable scripts/directories, and &lt;code&gt;644&lt;/code&gt; for regular files.&lt;/p&gt;

&lt;h3&gt;
  
  
  Changing the owner: &lt;code&gt;chown&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;chmod&lt;/code&gt; says &lt;strong&gt;what&lt;/strong&gt; can be done with a file. &lt;code&gt;chown&lt;/code&gt; says &lt;strong&gt;whose&lt;/strong&gt; it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sudo chown ubuntu:ubuntu configs/app.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sets &lt;code&gt;ubuntu&lt;/code&gt; as owner and &lt;code&gt;ubuntu&lt;/code&gt; as group. For a whole tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sudo chown -R deploy:deploy /srv/app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-R&lt;/code&gt; is recursive, same as in &lt;code&gt;cp&lt;/code&gt; and &lt;code&gt;rm&lt;/code&gt;, with the same warning attached: pointed at the wrong path it changes the owner of half the machine. Check it first with &lt;code&gt;ls -l&lt;/code&gt;, exactly as you did before deleting anything.&lt;/p&gt;

&lt;p&gt;In DevOps you'll reach for it mostly when deploying: files arrive owned by one user and the service runs as another, and until the owner matches, the process cannot write its own logs. Which is why &lt;code&gt;Permission denied&lt;/code&gt; rarely means “Linux has it in for you”: it means the file belongs to somebody else.&lt;/p&gt;

&lt;p&gt;Permissions are clearer now. The last missing piece is finding things, because a real server never leaves important files neatly on the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Finding files and searching inside them
&lt;/h2&gt;

&lt;p&gt;Find files by name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find ~/devops-course -name &lt;span class="s2"&gt;"*.md"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find directories:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find ~/devops-course -type d
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find recently modified files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;find ~/devops-course -type f -mtime -7
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;find&lt;/code&gt; looks rough at first. Then you discover it can locate almost anything if you give it clear criteria. It is like a tracking dog, but with syntax from the seventies.&lt;/p&gt;

&lt;p&gt;Search text inside files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;grep -r &lt;span class="s2"&gt;"Linux"&lt;/span&gt; ~/devops-course/notes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful options:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;grep -ri &lt;span class="s2"&gt;"linux"&lt;/span&gt; ~/devops-course/notes
grep -rn &lt;span class="s2"&gt;"Linux"&lt;/span&gt; ~/devops-course
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-r&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;searches recursively&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-i&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;ignores case&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-n&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;shows line numbers&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Later you'll use more modern tools like &lt;code&gt;rg&lt;/code&gt; (&lt;code&gt;ripgrep&lt;/code&gt;) because they are extremely fast and pleasant. But &lt;code&gt;grep&lt;/code&gt; is everywhere. On a minimal machine, in a tiny container, on a server with a 2014 personality: &lt;code&gt;grep&lt;/code&gt; is usually there. Learn it first. Add the turbo later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guided practice: prepare an operations structure
&lt;/h2&gt;

&lt;p&gt;Let's put everything together in a small practice. Create a structure simulating an operations project:&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; ~/devops-course
mkdir -p projects/webapp/&lt;span class="o"&gt;{&lt;/span&gt;bin,config,logs,tmp&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a configuration file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cat &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; projects/webapp/config/app.env &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s1"&gt;'EOF'&lt;/span&gt;
&lt;span class="nv"&gt;APP_ENV&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;development
&lt;span class="nv"&gt;APP_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;8080
&lt;span class="nv"&gt;LOG_LEVEL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;info
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cat &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; projects/webapp/bin/start.sh &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s1"&gt;'EOF'&lt;/span&gt;
&lt;span class="c"&gt;#!/usr/bin/env bash&lt;/span&gt;
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'Starting webapp on port 8080\n'&lt;/span&gt;
EOF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make it executable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;chmod +x projects/webapp/bin/start.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate a fake log:&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;printf&lt;/span&gt; &lt;span class="s1"&gt;'[INFO] Application started\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; projects/webapp/logs/app.log
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'[WARN] Cache is empty\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; projects/webapp/logs/app.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the structure:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tree projects/webapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;projects/webapp
├── bin
│   └── start.sh
├── config
│   └── app.env
├── logs
│   └── app.log
└── tmp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run the script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;./projects/webapp/bin/start.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Search configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;grep -n &lt;span class="s2"&gt;"APP_PORT"&lt;/span&gt; projects/webapp/config/app.env
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tail -n 10 projects/webapp/logs/app.log
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check permissions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ls -l projects/webapp/bin/start.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This practice looks small, but it contains half of operational life: structure, configuration, scripts, logs, permissions, and search. Change the names and you're already close to what you'll do with real services.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you should take from this lesson
&lt;/h2&gt;

&lt;p&gt;Linux becomes less scary when you stop seeing it as a black box and start seeing it as a system with a map.&lt;/p&gt;

&lt;p&gt;Keep this:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/&lt;/code&gt; is the root of the filesystem.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/home&lt;/code&gt;, &lt;code&gt;/etc&lt;/code&gt;, &lt;code&gt;/var&lt;/code&gt;, &lt;code&gt;/tmp&lt;/code&gt;, &lt;code&gt;/usr&lt;/code&gt;, and &lt;code&gt;/opt&lt;/code&gt; have common responsibilities.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pwd&lt;/code&gt;, &lt;code&gt;ls&lt;/code&gt;, and &lt;code&gt;cd&lt;/code&gt; provide basic orientation.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mkdir&lt;/code&gt;, &lt;code&gt;touch&lt;/code&gt;, &lt;code&gt;cp&lt;/code&gt;, &lt;code&gt;mv&lt;/code&gt;, and &lt;code&gt;rm&lt;/code&gt; manipulate files and directories.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;cat&lt;/code&gt;, &lt;code&gt;head&lt;/code&gt;, &lt;code&gt;tail&lt;/code&gt;, and &lt;code&gt;less&lt;/code&gt; let you read without editing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;chmod&lt;/code&gt; controls permissions; &lt;code&gt;rwx&lt;/code&gt; is not decoration.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;find&lt;/code&gt; and &lt;code&gt;grep&lt;/code&gt; help you locate files and content.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rm -rf&lt;/code&gt; is used with respect, not faith.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Challenge&lt;/strong&gt;: Inside &lt;code&gt;~/devops-course/projects&lt;/code&gt;, create an &lt;code&gt;api-service&lt;/code&gt; directory with &lt;code&gt;bin&lt;/code&gt;, &lt;code&gt;config&lt;/code&gt;, and &lt;code&gt;logs&lt;/code&gt; subdirectories. Add an executable &lt;code&gt;bin/healthcheck.sh&lt;/code&gt; script that prints &lt;code&gt;OK&lt;/code&gt;, a &lt;code&gt;config/service.env&lt;/code&gt; file with &lt;code&gt;SERVICE_PORT=3000&lt;/code&gt;, and a &lt;code&gt;logs/service.log&lt;/code&gt; file with two lines. Then use &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;ls -l&lt;/code&gt;, and &lt;code&gt;tail&lt;/code&gt; to prove everything exists and makes sense.&lt;/p&gt;

&lt;p&gt;In the next tutorial we'll continue with Linux, but one level higher: processes, services, basic networking, and system logs. In other words, the part where the machine stops being “files in folders” and starts behaving like a living system that sometimes needs care, sometimes needs a restart, and sometimes needs you to read &lt;code&gt;journalctl&lt;/code&gt; before blaming Kubernetes.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>linux</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Verifying and understanding AI-generated code</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Mon, 09 Nov 2026 15:30:37 +0000</pubDate>
      <link>https://dev.to/fj_palacios/verifying-and-understanding-ai-generated-code-5ghg</link>
      <guid>https://dev.to/fj_palacios/verifying-and-understanding-ai-generated-code-5ghg</guid>
      <description>&lt;p&gt;AI gives you a beautiful function. Clear names, type hints, decent structure. It even looks like it was written by someone who slept eight hours, which already puts it ahead of a worrying amount of human Tuesday-afternoon code.&lt;/p&gt;

&lt;p&gt;You copy it, paste it, run something quick, and it works.&lt;/p&gt;

&lt;p&gt;Then the uncomfortable question arrives: can you explain every line? Do you know what happens with weird input? Did you check for an &lt;code&gt;except Exception: pass&lt;/code&gt; tucked away like a roach under the fridge? Or are you about to add code to your project because it “looks fine” and has nice hair?&lt;/p&gt;

&lt;p&gt;This is one of the most important rules of working with AI: &lt;strong&gt;never accept code you don't understand&lt;/strong&gt;. Not because AI is bad. Not because every line must be handcrafted with artisanal suffering as if programming were stone carving. But because once code enters your repo, it becomes your responsibility. The blame will not say “a very confident model wrote this.” It will say your name. How thoughtful.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/en/tutorials/iteration-refinement-ai-dialogue"&gt;previous lesson we looked at refining an AI conversation without losing control&lt;/a&gt;. Now comes the professional filter: deciding whether generated code deserves to enter your project or needs to go back in the oven.&lt;/p&gt;

&lt;h2&gt;
  
  
  The “can I explain this?” test
&lt;/h2&gt;

&lt;p&gt;The first filter is not tests. It is more uncomfortable: &lt;strong&gt;explaining the code in your own words&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If AI generates this:&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;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;group_orders_by_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]]:&lt;/span&gt;
    &lt;span class="n"&gt;grouped_orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;order&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;grouped_orders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;grouped_orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before using it, you should be able to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What does &lt;code&gt;defaultdict(list)&lt;/code&gt; do?&lt;/li&gt;
&lt;li&gt;Why is it converted to &lt;code&gt;dict&lt;/code&gt; at the end?&lt;/li&gt;
&lt;li&gt;What happens if an order does not have &lt;code&gt;user_id&lt;/code&gt;?&lt;/li&gt;
&lt;li&gt;What happens if &lt;code&gt;user_id&lt;/code&gt; is a string?&lt;/li&gt;
&lt;li&gt;Does the function modify the original objects or only group them?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you can't answer, that's fine. Really. The problem is not not knowing; the problem is pretending you do because the code looks nice. That is the path to production with a flashlight and no batteries.&lt;/p&gt;

&lt;p&gt;You can use AI itself to explain the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Explain this code line by line.

For each line or block:
- What it does
- Why it is necessary
- What it assumes about the input data
- What could fail

Do not suggest improvements yet. Only explain.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That “do not suggest improvements yet” matters. Without it, AI may jump into refactoring before you understand the current version. Then you have two problems: the original code you didn't understand and a new version you also don't understand. The worst kind of two-for-one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make AI declare its assumptions
&lt;/h2&gt;

&lt;p&gt;Generated code always contains assumptions. Some are reasonable. Others are the technical equivalent of “surely nobody will pass a string here.” Spoiler: somebody passed a string.&lt;/p&gt;

&lt;p&gt;Ask AI to bring those assumptions into the open:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;List every assumption this code makes.

Classify them as:
1. Assumptions about input data
2. Assumptions about the environment
3. Assumptions about performance
4. Assumptions about security

For each assumption, explain what would happen if it is false.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the previous function, a good review should point out things like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Each &lt;code&gt;order&lt;/code&gt; must be a &lt;code&gt;dict&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Each &lt;code&gt;order&lt;/code&gt; must contain the &lt;code&gt;user_id&lt;/code&gt; key&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;user_id&lt;/code&gt; must be an integer if you want the return type to be true&lt;/li&gt;
&lt;li&gt;The list must fit in memory&lt;/li&gt;
&lt;li&gt;There is no structure validation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice what just happened: the function did not change, but your understanding did. Now you know where the corners are. And in programming, corners matter, because that is where the bug sits with a coffee and infinite patience.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests are not decoration
&lt;/h2&gt;

&lt;p&gt;The second filter is tests. Not as a religious ceremony, but as a concrete way to say: “this does what I think it does.”&lt;/p&gt;

&lt;p&gt;For the function above, you can ask for tests 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;Write pytest tests for this function.

Cover:
- Empty list
- One order
- Multiple orders for the same user
- Orders for different users
- Order without user_id
- user_id with the wrong type
- Large list (100,000 orders): time should grow linearly, not quadratically

Do not change the implementation yet.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The last line matters again. First you want tests that describe behavior and edge cases. Then you decide whether the implementation needs to change.&lt;/p&gt;

&lt;p&gt;An initial set could be:&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;import&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;group_orders_by_user&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_groups_orders_by_user_id&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;25.0&lt;/span&gt;&lt;span class="p"&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;15.0&lt;/span&gt;&lt;span class="p"&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;user_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;40.0&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;group_orders_by_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;orders&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;20&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;orders&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="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_returns_empty_dict_for_empty_list&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;group_orders_by_user&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_raises_key_error_when_user_id_is_missing&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="o"&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;id&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;25.0&lt;/span&gt;&lt;span class="p"&gt;}]&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;group_orders_by_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Are these tests perfect? No. Are they better than “I tried one example and it seems fine”? Massively. That “seems fine” has funded more overtime than many consulting companies.&lt;/p&gt;

&lt;p&gt;If you are also following the &lt;a href="https://dev.to/en/courses/learn-to-code-from-scratch-python"&gt;Python from scratch course&lt;/a&gt;, this connects to something fundamental: a function is not just the code you write, it is the contract it fulfills. Tests make that contract visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Red flags that are not negotiable
&lt;/h2&gt;

&lt;p&gt;Some signals should make you stop. Not “I'll check later.” Not “surely it doesn't matter.” Stop.&lt;/p&gt;

&lt;p&gt;AI can generate reasonable code and still sneak in dangerous things. Especially if the prompt was broad, the context was incomplete, or you asked for “production-ready” code, that phrase that summons demons with Scrum certification.&lt;/p&gt;

&lt;p&gt;Look for this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;TODO&lt;/code&gt; inside critical logic.&lt;/strong&gt;&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;charge_customer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# TODO: implement payment retry logic
&lt;/span&gt;    &lt;span class="n"&gt;payment_provider&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;charge&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;TODO&lt;/code&gt; here is not an innocent note. It is debt with interest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Silently swallowed exceptions.&lt;/strong&gt;&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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;send_invoice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;invoice&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not error handling. It is putting duct tape over the red warning light in your car. The problem is still there, but now you also can't see it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardcoded secrets.&lt;/strong&gt;&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="n"&gt;API_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sk_live_123456&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No. Just no. Not for testing. Not “I'll change it later.” Git has memory. GitHub too. The internet, unfortunately, has better memory than some product teams.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SQL built by string concatenation.&lt;/strong&gt;&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="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM users WHERE email = &lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"'"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this appears, an alarm should go off. Use parameterized queries. SQL injection is not an urban legend seniors tell juniors to scare them; it still works because we still write things like this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTML built by gluing user data together.&lt;/strong&gt;&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;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;p&amp;gt;Order for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same mistake in a different suit. If that email comes from a form, someone can drop a &lt;code&gt;&amp;lt;script&amp;gt;&lt;/code&gt; in there and wait. Templates that escape by default (Jinja, React) have your back; the moment a &lt;code&gt;|safe&lt;/code&gt;, an &lt;code&gt;innerHTML&lt;/code&gt; or a &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; shows up, you need to know exactly why it is there. And “the AI wrote it” is not a reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complex regex without explanation.&lt;/strong&gt;&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="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&amp;amp;])[A-Za-z\d@$!%*?&amp;amp;]{8,}$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It might be correct. It might also be a trap shaped like a hieroglyph. If AI generates a complex regex, ask it to explain it and add tests. If you cannot understand it, don't place it in critical logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  The serious review prompt
&lt;/h2&gt;

&lt;p&gt;After understanding the code and getting basic tests, ask for a review with professional suspicion. Not so AI can gently critique itself with corporate softness, but so it searches for concrete problems.&lt;/p&gt;

&lt;p&gt;Use something 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;Review this code as if you were responsible for approving it in a code review.

Look specifically for:
1. Functional bugs
2. Missing edge cases
3. Security risks (SQL injection, XSS, hardcoded secrets)
4. Performance problems
5. Hidden assumptions
6. Unnecessarily complex code
7. Differences between what the code does and what the name promises

For each finding, answer with:
- Severity: high, medium, or low
- Concrete evidence in the code
- Why it matters
- Minimal recommended change

Do not rewrite the whole code unless it is strictly necessary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure avoids two common problems: AI replying “looks good” with a plastic smile, or rewriting everything because it got excited. You want findings, evidence, and minimal changes. Surgery, not a full bathroom renovation.&lt;/p&gt;

&lt;p&gt;You can also ask it to compare against the tests:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Compare the implementation with the tests.

Indicate:
- Which behaviors are covered
- Which behaviors are not covered
- Which test you would add before modifying the implementation
- Whether any test asserts questionable behavior
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially useful because tests can also be wrong. Unpleasant surprise: AI can generate tests that verify exactly the bug it just invented. Green tests, wrong code. All very elegant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding does not mean memorizing everything
&lt;/h2&gt;

&lt;p&gt;There is a psychological trap here: thinking that “understanding the code” means being able to rewrite it from memory on a whiteboard while somebody stares at you like you're in the interview.&lt;/p&gt;

&lt;p&gt;No.&lt;/p&gt;

&lt;p&gt;Understanding means you can explain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What problem it solves&lt;/li&gt;
&lt;li&gt;What inputs it expects&lt;/li&gt;
&lt;li&gt;What outputs it promises&lt;/li&gt;
&lt;li&gt;What errors it can raise&lt;/li&gt;
&lt;li&gt;What important decisions it makes&lt;/li&gt;
&lt;li&gt;Which cases are still not covered&lt;/li&gt;
&lt;li&gt;Which part still makes you uncertain&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is key. A professional developer is not someone who never has doubts; it is someone who can locate them. “I don't understand this part” is not a defeat. It is a sign that you should not merge yet. Much better than the classic “I think it's fine,” a sentence that should come with a fire extinguisher.&lt;/p&gt;

&lt;p&gt;A useful practice is asking for a reverse explanation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Ask me questions to check whether I understand this code.

Start with easy questions and increase the difficulty.
Do not give me the answers until I respond.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns AI into an examiner, not a generator. And it is uncomfortable in the right way: it forces you to demonstrate understanding, not just nod while reading.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to accept, refine, or reject
&lt;/h2&gt;

&lt;p&gt;After reviewing, you have three options.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Accept&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You understand the code&lt;/li&gt;
&lt;li&gt;Tests cover the important behaviors&lt;/li&gt;
&lt;li&gt;There are no serious red flags&lt;/li&gt;
&lt;li&gt;It fits the project's conventions&lt;/li&gt;
&lt;li&gt;The change is simpler than the problem it solves&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Refine&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The idea is correct, but validation is missing&lt;/li&gt;
&lt;li&gt;Naming is confusing&lt;/li&gt;
&lt;li&gt;Tests are insufficient&lt;/li&gt;
&lt;li&gt;There is a clear edge case to add&lt;/li&gt;
&lt;li&gt;The implementation works, but is more complex than necessary&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Reject and start fresh&lt;/strong&gt; when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You cannot explain an important part&lt;/li&gt;
&lt;li&gt;The architecture does not fit&lt;/li&gt;
&lt;li&gt;There are structural security risks&lt;/li&gt;
&lt;li&gt;AI assumed false requirements&lt;/li&gt;
&lt;li&gt;Fixing it would cost more than asking for a new version&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This decision matters more than the perfect prompt. The perfect prompt does not exist. What exists is judgment. And judgment is trained by reviewing, testing, rejecting, and asking again with better constraints. Yes, it is more work than copy-paste. It also comes with fewer surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key concepts from this lesson
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;AI-generated code enters your repo under your responsibility&lt;/li&gt;
&lt;li&gt;If you cannot explain an important line, you should not accept it yet&lt;/li&gt;
&lt;li&gt;Ask AI to declare assumptions before refactoring&lt;/li&gt;
&lt;li&gt;Tests turn “it seems to work” into verifiable behavior&lt;/li&gt;
&lt;li&gt;Security red flags, secrets, SQL injection, and swallowed errors are not negotiable&lt;/li&gt;
&lt;li&gt;A good review asks for evidence and minimal changes, not theatrical rewrites&lt;/li&gt;
&lt;li&gt;Accepting, refining, or rejecting is a technical decision, not an emotional one&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;AI can write code quickly, but speed without judgment only gets you to the bug faster. Your job is not to compete with AI by writing more lines; it is to direct, understand, and decide what deserves to live in the project.&lt;/p&gt;

&lt;p&gt;In the next lesson we'll zoom out and look at the AI tool landscape for developers — chats, IDE integrations, terminal agents, and APIs — and when each one makes sense without turning your workflow into a subscription flea market.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Iteration and refinement: the AI dialogue</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Tue, 27 Oct 2026 11:42:23 +0000</pubDate>
      <link>https://dev.to/fj_palacios/iteration-and-refinement-the-ai-dialogue-ep1</link>
      <guid>https://dev.to/fj_palacios/iteration-and-refinement-the-ai-dialogue-ep1</guid>
      <description>&lt;p&gt;You ask AI for a small function. Nothing dramatic. A quiet little function that fits on screen and minds its own business.&lt;/p&gt;

&lt;p&gt;Three messages later, the function has logging, validation, caching, metrics, timezone support, a &lt;code&gt;Manager&lt;/code&gt; class, two custom exceptions, and the unmistakable feeling that someone has lost control of the construction site.&lt;/p&gt;

&lt;p&gt;When did that happen? Which exact prompt turned a function into a seed-stage startup? Was it you? Was it the AI? Was it enthusiasm? Yes.&lt;/p&gt;

&lt;p&gt;Working with AI is not about writing one perfect prompt and waiting for code to fall from the sky smelling faintly of fresh coffee. It's a &lt;strong&gt;refinement dialogue&lt;/strong&gt;. You ask for something, evaluate the answer, correct it, tighten it, cut it down, and ask again. Done well, the result improves step by step. Done badly, you end up with a long, contradictory conversation and code that no longer remembers why it was born.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/en/tutorials/prompts-for-code-generation-ai"&gt;previous lesson we looked at generating code with AI without asking “make me things” and hoping for the best&lt;/a&gt;. Now comes the part immediately after that: what to do when the first answer is not enough. Which is most of the time. Mild surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The first answer is rarely the right one
&lt;/h2&gt;

&lt;p&gt;The first AI response is usually a &lt;strong&gt;draft&lt;/strong&gt;. Sometimes a good draft. Sometimes a draft wearing clown shoes. But still a starting point, not a stone tablet carried down from a mountain.&lt;/p&gt;

&lt;p&gt;This matters because many people treat the first answer like a pass/fail exam: if it's wrong, “AI is useless”; if it's right, they copy it; if it's half-right, they copy it anyway but with a worried expression. None of those is a professional workflow.&lt;/p&gt;

&lt;p&gt;A healthier flow 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;Round 1:
"Create a basic working version. Prioritize clarity over optimization."

Round 2:
"Now handle the edge case where the list is empty."

Round 3:
"This version loops over the list twice. Refactor it to use a single pass."

Round 4:
"The code works, but the names don't explain intent. Improve names without changing behavior."

Round 5:
"Add tests for the normal case, empty list, and duplicate values."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the structure: each round changes &lt;strong&gt;one specific thing&lt;/strong&gt;. You don't say “improve it.” “Improve it” is a black box. It could mean performance, readability, architecture, security, naming, tests, or the AI deciding to introduce a Strategy pattern because apparently today we are doing Enterprise Java.&lt;/p&gt;

&lt;p&gt;Good refinement means directing. Not vaguely nudging.&lt;/p&gt;

&lt;h2&gt;
  
  
  The refinement loop
&lt;/h2&gt;

&lt;p&gt;The basic pattern has four steps:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Ask&lt;/strong&gt; for a concrete version&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evaluate&lt;/strong&gt; what works and what doesn't&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Correct&lt;/strong&gt; one specific dimension&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Verify&lt;/strong&gt; that the previous behavior still holds&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The part people skip is the third or the fourth. Usually both, because humans are optimistic until production educates them.&lt;/p&gt;

&lt;p&gt;Look at this example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I want a Python function that receives a list of prices and returns the total
including tax. The tax rate should be configurable. Prioritize clarity.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;AI might return this:&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;calculate_total_with_tax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tax_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.21&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total&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;for&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tax_rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not bad. But maybe you want type hints and validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Keep the same logic, but add type hints.
If any price is negative, raise ValueError.
Do not change the function name.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;Do not change the function name&lt;/code&gt; looks silly until you don't include it. Then the AI decides &lt;code&gt;calculate_total_with_tax&lt;/code&gt; is now called &lt;code&gt;compute_invoice_amount&lt;/code&gt; because it sounds more professional. And yes, it does sound more professional. It also just broke all your tests.&lt;/p&gt;

&lt;p&gt;A refined answer might be:&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;calculate_total_with_tax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;tax_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.21&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;prices&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;price&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Prices cannot be negative&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prices&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;total&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tax_rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Better. Now verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Check whether the change preserves the previous behavior.
Compare the original version and the new one for these cases:
1. [10, 20]
2. []
3. [0]
4. [-5]

Format:
- Case
- Original result
- New result
- Relevant difference
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This step is not ceremonial. It's the equivalent of looking both ways before crossing the street. Most days nothing happens. On the day something does, you're glad you weren't crossing while staring at your phone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Change one thing at a time
&lt;/h2&gt;

&lt;p&gt;The golden rule of refinement is simple: &lt;strong&gt;one dimension per round&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Don't do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Make it faster, cleaner, add tests, handle errors, use better architecture,
explain the changes, and make it production-ready.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That prompt looks productive. It is actually six conversations stuffed into one and asked politely not to collide. What happens if the result gets worse? Was it the optimization? The architecture? The tests? The moon in Capricorn? Good luck debugging that.&lt;/p&gt;

&lt;p&gt;Do this instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First, review readability only.
Do not change behavior.
Do not optimize yet.
Return only the refactored version and a list of naming changes.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Now review performance.
Keep the same behavior and the same public interface.
Point out any trade-off before changing the code.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Now add tests.
Do not modify the implementation unless you find a real bug.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not slower. It's how you avoid rebuilding the whole cabinet because you tightened three screws at once and now the door closes like it has unresolved resentment.&lt;/p&gt;

&lt;p&gt;Good refinement leaves a trail. You know what changed, why it changed, and what was not supposed to change.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to refine and when to start fresh
&lt;/h2&gt;

&lt;p&gt;Here's a decision that separates beginner AI usage from professional AI usage: not every conversation deserves to be rescued.&lt;/p&gt;

&lt;p&gt;Refine when the base is good:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The general idea fits&lt;/li&gt;
&lt;li&gt;The code works on the happy path&lt;/li&gt;
&lt;li&gt;The structure makes sense&lt;/li&gt;
&lt;li&gt;The problems are localized&lt;/li&gt;
&lt;li&gt;You can clearly describe what you want changed&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The general solution works for me.
Refine only the error handling: right now it silently returns None,
but I want it to raise ValueError with a clear message.
Do not change the function signature.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Start fresh when the foundation is crooked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The architecture doesn't fit your project&lt;/li&gt;
&lt;li&gt;The AI assumed false requirements&lt;/li&gt;
&lt;li&gt;The conversation now mixes contradictory decisions&lt;/li&gt;
&lt;li&gt;You've corrected the same thing three times and it keeps coming back&lt;/li&gt;
&lt;li&gt;The code works, but by accident and with a damp basement smell&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's when you cut:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Let's start over.
Ignore the previous approach.

New goal:
[clear description]

Constraints:
- [constraint 1]
- [constraint 2]
- [constraint 3]

Do not reuse code from the previous response unless I explicitly ask for it.
Before writing code, summarize the proposed design in 5 bullets.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;“Ignore the previous approach” is not magic, but it helps. The important part is that you provide a clean new specification. You are not trying to patch a conversation that now looks like a 73-message Slack thread where nobody knows whether we're still discussing the original bug.&lt;/p&gt;

&lt;p&gt;If you're unsure, use this rule: if you can explain the change in one sentence, refine. If you need to explain the entire history of how you got there, start fresh.&lt;/p&gt;

&lt;h2&gt;
  
  
  Managing long conversations
&lt;/h2&gt;

&lt;p&gt;Long AI conversations have a problem: context gets diluted. It doesn't disappear all at once, but it loses weight. Important decisions compete with old details, discarded changes, temporary examples, and that response from twenty minutes ago where you asked for “just a quick test.” Yes, that quick test now lives in the context like inherited furniture.&lt;/p&gt;

&lt;p&gt;To stay in control, use &lt;strong&gt;checkpoints&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Every few rounds, ask for an operational summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Summarize the current state of the solution.

Format:
1. Original goal
2. Decisions made
3. Current requirements
4. Constraints that must NOT change
5. Code or files affected
6. Pending work
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then review the summary. This matters: &lt;strong&gt;do not accept the summary without reading it&lt;/strong&gt;. AI can confidently summarize things you never decided. Very human skill, actually; it also happens in meetings.&lt;/p&gt;

&lt;p&gt;If the summary is correct, use it as the new anchor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Use this summary as the main context for the next responses.
If anything contradicts earlier messages, prioritize this summary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you're opening a new conversation, paste your own summary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Working context:
- I'm implementing [feature]
- We already decided [decision]
- We don't want [constraint]
- The expected behavior is [behavior]
- The current problem is [problem]

Task:
Help me continue from here.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This reduces noise dramatically. Not because AI has “perfect memory,” but because you're putting the map back in front of it. And trust me: in long conversations, the map is not optional. It's the difference between reaching the destination and optimizing a function nobody uses anymore.&lt;/p&gt;

&lt;h2&gt;
  
  
  The refinement template
&lt;/h2&gt;

&lt;p&gt;Save this template. It's simple, but it prevents 80% of the chaos:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original goal:
[what I wanted to achieve]

Current response:
[paste the relevant part, not the whole novel]

What works:
- [point 1]
- [point 2]

What I want changed:
- [specific change]

Constraints:
- Do not change [interface / name / behavior / format]
- Keep [convention / style / dependency]
- Do not add [thing you don't want]

Response format:
1. Briefly explain the change
2. Return the refined version
3. List what stayed the same
4. Mention any trade-off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Original goal:
Create a function that groups users by country.

Current response:
[paste function]

What works:
- The output shape is correct
- The normal case works

What I want changed:
- Handle users without a country using the key "unknown"

Constraints:
- Do not change the name group_users_by_country
- Do not change the output format
- Do not add dependencies
- Do not turn this into a class

Response format:
1. Briefly explain the change
2. Return the refined function
3. List what stayed the same
4. Mention any trade-off
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sentence “Do not turn this into a class” belongs in a museum. Not because classes are bad, but because AI has a habit of putting formal clothes on problems that were perfectly fine in a t-shirt.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ask for differences, not just new versions
&lt;/h2&gt;

&lt;p&gt;When refining, we often ask for “the improved version.” That's fine, but one piece is often missing: understanding &lt;strong&gt;what changed&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Ask for a conceptual diff:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Before giving me the final code, summarize the changes from the previous version:
- What changed
- What stayed the same
- Why the change improves the solution
- What risk it introduces, if any
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This forces you to review with judgment. If the AI says “the signature stayed the same” and you can see the signature changed, you now know not to trust that answer without inspection. Excellent: you found the problem before pasting code.&lt;/p&gt;

&lt;p&gt;You can also ask it to show only the relevant changes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Return only the modified parts.
Do not repeat the whole file unless necessary.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is especially useful when working with large files. If every response repeats 300 lines, your conversation fills with noise and you end up scrolling like you're searching for a hidden clause in a phone contract.&lt;/p&gt;

&lt;p&gt;Less text does not always mean less quality. Sometimes it means less surface area for things to break unnoticed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't let AI move the goalposts
&lt;/h2&gt;

&lt;p&gt;The biggest danger in refinement is not that AI makes a mistake. We already expect that. The real danger is that it slowly changes the goal and you don't notice.&lt;/p&gt;

&lt;p&gt;You start with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I want to validate an email.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five rounds later you're discussing users, allowed domains, normalization, DNS verification, rate limiting, and a notification service. All of it may be reasonable. It may also be completely unnecessary.&lt;/p&gt;

&lt;p&gt;That's why it's useful to repeat the original goal during the conversation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Remember the goal: validate the format of an email on the frontend.
We are not implementing user registration.
Do not add domain verification.
Do not introduce network calls.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is not being rude. It's directing. AI does not pay the maintenance cost of keeping a system simple. You do. You will maintain that code when the excitement is gone and only the repository remains.&lt;/p&gt;

&lt;p&gt;A good warning sign: if a response introduces an abstraction you didn't ask for, pause. Ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Why did you introduce this abstraction?
What concrete problem does it solve in this case?
What would the simplest version look like without it?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Sometimes the abstraction will be justified. Often it won't. That's where you need to act like an architect, not a spectator. AI proposes; you decide. If you reverse that order, your code starts looking like a house designed by someone paid per hallway.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key concepts from this lesson
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;The first AI response is usually a draft, not the final result&lt;/li&gt;
&lt;li&gt;Good refinement changes one concrete dimension per round&lt;/li&gt;
&lt;li&gt;“Improve it” is too vague; ask for specific, verifiable changes&lt;/li&gt;
&lt;li&gt;Refine when the base is good; start fresh when the approach is crooked&lt;/li&gt;
&lt;li&gt;In long conversations, use checkpoints and summaries you actually review&lt;/li&gt;
&lt;li&gt;Ask what changed and what stayed the same, not just for a new version&lt;/li&gt;
&lt;li&gt;Repeat the original goal to prevent the conversation from moving the goalposts&lt;/li&gt;
&lt;li&gt;AI proposes; you direct and decide&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge:&lt;/strong&gt; Take an AI response you've used recently and run one controlled refinement round. Change only one thing: readability, performance, error handling, or tests. Before accepting the result, ask what changed, what stayed the same, and what risk it introduces.&lt;/p&gt;

&lt;p&gt;Refinement is where AI stops being an answer machine and starts becoming a real working tool. But one critical piece is still missing: understanding and verifying the code it generates. In the next lesson we'll review code line by line, spot red flags, and avoid trusting code that works only because God is good.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>ai</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Scope and namespaces in Python: where your variables live</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Mon, 26 Oct 2026 07:43:49 +0000</pubDate>
      <link>https://dev.to/fj_palacios/scope-and-namespaces-in-python-where-your-variables-live-1cjo</link>
      <guid>https://dev.to/fj_palacios/scope-and-namespaces-in-python-where-your-variables-live-1cjo</guid>
      <description>&lt;p&gt;You have a variable called &lt;code&gt;counter&lt;/code&gt;. You can see it. It's right there. You wrote it yourself, probably after a coffee that stopped being warm twenty minutes ago. You run the program and Python tells you it has no idea what variable you're talking about.&lt;/p&gt;

&lt;p&gt;Excuse me? Didn't we just see it? Did it leave the building? Do variables have private lives now?&lt;/p&gt;

&lt;p&gt;Welcome to &lt;strong&gt;scope&lt;/strong&gt;: the part of Python that decides where each name can be seen from. It isn't black magic, although at first it has the same energy as looking for a missing sock inside a washing machine that swears it knows nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  What scope is
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Scope&lt;/strong&gt; is the area of a program where a name exists and can be used. That name can be a variable, a function, a parameter, or anything Python can reference.&lt;/p&gt;

&lt;p&gt;Look at this example:&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;greet&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first part works:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;But the last line fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NameError: name 'message' is not defined
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;message&lt;/code&gt; exists inside &lt;code&gt;greet()&lt;/code&gt;, but not outside it. It is a &lt;strong&gt;local&lt;/strong&gt; variable of that function. From the outside, Python can't see it.&lt;/p&gt;

&lt;p&gt;Think of a room with a whiteboard inside. People in the room can read what's on the whiteboard. People outside can't. Shouting louder does not change the building architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Local variables
&lt;/h2&gt;

&lt;p&gt;A variable created inside a function lives in that function's local scope:&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;calculate_total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.21&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;tax&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;

&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculate_total&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;price&lt;/code&gt;, &lt;code&gt;tax&lt;/code&gt;, and &lt;code&gt;total&lt;/code&gt; are local to &lt;code&gt;calculate_total()&lt;/code&gt;. They exist while the function runs. When the function finishes, its scope disappears.&lt;/p&gt;

&lt;p&gt;That does not mean the returned value disappears. &lt;code&gt;return total&lt;/code&gt; gives the value back to the outside code, and that value is stored in &lt;code&gt;result&lt;/code&gt;. But the name &lt;code&gt;total&lt;/code&gt; itself belonged to the function.&lt;/p&gt;

&lt;p&gt;This matters: &lt;strong&gt;returning a value does not export the variable&lt;/strong&gt;. It exports the value. The local variable stays home, like that friend who says "I'll join later" and absolutely does not join later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Global variables
&lt;/h2&gt;

&lt;p&gt;A variable defined outside any function lives in the module's global scope:&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="n"&gt;tax_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.21&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&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;price&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tax_rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;calculate_total&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





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

&lt;/div&gt;



&lt;p&gt;The function can &lt;strong&gt;read&lt;/strong&gt; &lt;code&gt;tax_rate&lt;/code&gt; because it does not find that name in its local scope, so it looks outside.&lt;/p&gt;

&lt;p&gt;So far, everything is calm. The shelf is standing. We haven't found the extra screw yet.&lt;/p&gt;

&lt;p&gt;The problem appears when you try to modify a global variable from inside a function:&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="n"&gt;counter&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;def&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fails:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;UnboundLocalError: cannot access local variable 'counter' where it is not associated with a value
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And here Python looks dramatic, but it has a reason. Because there is an assignment to &lt;code&gt;counter&lt;/code&gt; inside the function, Python decides that &lt;code&gt;counter&lt;/code&gt; is local to that function. Then, when it evaluates &lt;code&gt;counter + 1&lt;/code&gt;, it tries to read the local variable before it has a value.&lt;/p&gt;

&lt;p&gt;Yes: the global variable exists. Yes: the name is the same. No: Python does not use it in that case. This is where things get interesting, which in programming usually means "you will stare at the screen in silence for thirty seconds."&lt;/p&gt;

&lt;h2&gt;
  
  
  The mental rule: Python searches from the inside out
&lt;/h2&gt;

&lt;p&gt;When Python sees a name, it looks for it through a chain of scopes. At this level, keep this idea:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;First it checks the local scope of the current function&lt;/li&gt;
&lt;li&gt;Then enclosing scopes if there are nested functions&lt;/li&gt;
&lt;li&gt;Then the module's global scope&lt;/li&gt;
&lt;li&gt;Finally built-in names like &lt;code&gt;print&lt;/code&gt;, &lt;code&gt;len&lt;/code&gt;, or &lt;code&gt;range&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This is usually summarized as &lt;strong&gt;LEGB&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;Letter&lt;/th&gt;
&lt;th&gt;Scope&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;L&lt;/td&gt;
&lt;td&gt;Local&lt;/td&gt;
&lt;td&gt;Inside the current function&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;E&lt;/td&gt;
&lt;td&gt;Enclosing&lt;/td&gt;
&lt;td&gt;Outer functions when functions are nested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;Global&lt;/td&gt;
&lt;td&gt;The current module&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B&lt;/td&gt;
&lt;td&gt;Built-in&lt;/td&gt;
&lt;td&gt;Names Python provides by default&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Don't stress about memorizing the acronym like it's a production password. The important part is the direction: Python starts nearby and moves outward.&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="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;global&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;local&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&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="nf"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight lua"&gt;&lt;code&gt;&lt;span class="kd"&gt;local&lt;/span&gt;
&lt;span class="n"&gt;global&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside &lt;code&gt;show()&lt;/code&gt;, the local name wins. Outside, the global one is used. Each scope has its own whiteboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a namespace is
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;namespace&lt;/strong&gt; is the map that connects names to objects.&lt;/p&gt;

&lt;p&gt;More simply: it's like an internal dictionary where Python stores things like this:&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="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&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;Ada&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;age&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;greet&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;function&lt;/span&gt; &lt;span class="n"&gt;greet&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You usually don't manipulate that dictionary directly when you're starting out, but it exists. And it helps explain why two scopes can have variables with the same name without overwriting each other.&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="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;outside&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;example&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inside&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;example&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;inside
outside
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There isn't one single &lt;code&gt;message&lt;/code&gt; variable fighting for its identity. There are two &lt;code&gt;message&lt;/code&gt; names in two different namespaces. Same text, different whiteboard.&lt;/p&gt;

&lt;p&gt;It's like having two folders called &lt;code&gt;final-project&lt;/code&gt; in two different directories. Not elegant, but the system understands it. Your brain may not, but the system does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The global keyword
&lt;/h2&gt;

&lt;p&gt;If you really want to modify a global variable from inside a function, you can use &lt;code&gt;global&lt;/code&gt;:&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="n"&gt;counter&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;def&lt;/span&gt; &lt;span class="nf"&gt;increment&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;counter&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1
2
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;global counter&lt;/code&gt; tells Python: "when I say &lt;code&gt;counter&lt;/code&gt; inside this function, I mean the global &lt;code&gt;counter&lt;/code&gt;."&lt;/p&gt;

&lt;p&gt;It works. Using a chair as a ladder also works. The question is whether you should.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;global&lt;/code&gt; is not evil because it exists, but it is often a sign that your function depends too much on external state. That makes code harder to test, harder to reuse, and easier to break from somewhere else.&lt;/p&gt;

&lt;p&gt;Compare this:&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="n"&gt;counter&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;def&lt;/span&gt; &lt;span class="nf"&gt;increment&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;counter&lt;/span&gt;
    &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this:&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;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counter&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;counter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





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

&lt;/div&gt;



&lt;p&gt;The second version is more explicit: the function receives a value and returns another one. It does not touch hidden things in another room. If something breaks tomorrow, you have fewer dark hallways to inspect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nested functions
&lt;/h2&gt;

&lt;p&gt;Python lets you define functions inside other functions:&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;create_greeting&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="n"&gt;prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_message&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;build_message&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;create_greeting&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ada&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello, Ada
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;build_message()&lt;/code&gt; can read &lt;code&gt;prefix&lt;/code&gt; and &lt;code&gt;name&lt;/code&gt;, even though they are not defined inside it. They are in the scope of &lt;code&gt;create_greeting()&lt;/code&gt;, which is its outer scope.&lt;/p&gt;

&lt;p&gt;That is the &lt;strong&gt;enclosing&lt;/strong&gt; scope in LEGB: an inner function can see names from the function that contains it.&lt;/p&gt;

&lt;p&gt;This appears in closures, decorators, and more advanced code. For now, keep the idea: a nested function can look outward, but not randomly. Python follows a specific route; it does not open drawers at random like you do when you're looking for your keys five minutes before leaving.&lt;/p&gt;

&lt;h2&gt;
  
  
  The nonlocal keyword
&lt;/h2&gt;

&lt;p&gt;Now for the delicate part. An inner function can read variables from the outer function, but if it wants to modify them it needs &lt;code&gt;nonlocal&lt;/code&gt;:&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;create_counter&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;counter&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;def&lt;/span&gt; &lt;span class="nf"&gt;increment&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;nonlocal&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;
        &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;counter&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;increment&lt;/span&gt;

&lt;span class="n"&gt;counter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;counter&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1
2
3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;nonlocal counter&lt;/code&gt; tells Python: "this &lt;code&gt;counter&lt;/code&gt; is not local to &lt;code&gt;increment()&lt;/code&gt;, but it is not global either; it lives in an outer function."&lt;/p&gt;

&lt;p&gt;Without &lt;code&gt;nonlocal&lt;/code&gt;, you would get the same &lt;code&gt;UnboundLocalError&lt;/code&gt; problem as before: Python would see an assignment to &lt;code&gt;counter&lt;/code&gt; inside &lt;code&gt;increment()&lt;/code&gt; and assume it is a local variable.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;nonlocal&lt;/code&gt; is used less often than normal variables, and that's fine. You don't need to put nested functions everywhere to feel professional. Sometimes clear code is just a normal function with clear parameters. Revolutionary, I know.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to avoid global and nonlocal
&lt;/h2&gt;

&lt;p&gt;The practical rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use local variables whenever you can.&lt;/li&gt;
&lt;li&gt;Pass data as parameters.&lt;/li&gt;
&lt;li&gt;Return results with &lt;code&gt;return&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;global&lt;/code&gt; rarely and with a clear reason.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;nonlocal&lt;/code&gt; when you are creating an inner function that needs to remember state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This pattern is usually better:&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;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;discount&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;price&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;final_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;apply_discount&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="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Than this:&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="n"&gt;discount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;
&lt;span class="n"&gt;final_price&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;def&lt;/span&gt; &lt;span class="nf"&gt;apply_discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&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;final_price&lt;/span&gt;
    &lt;span class="n"&gt;final_price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;apply_discount&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;final_price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second version works, but it leaves side effects behind. The function doesn't just calculate: it also modifies a global variable. It's the kind of code that looks innocent until it grows, multiplies, and one day someone asks why &lt;code&gt;final_price&lt;/code&gt; is changing without being invited to the party.&lt;/p&gt;

&lt;h2&gt;
  
  
  Common scope mistakes
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Thinking a local variable exists outside
&lt;/h3&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;create_user&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ada&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NameError: name 'name' is not defined
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fix: return the value.&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;create_user&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ada&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;

&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_user&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Modifying a global without global
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;total&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;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fails because Python treats &lt;code&gt;total&lt;/code&gt; as local inside &lt;code&gt;add()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Better solution at this level: avoid global state.&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;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;price&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;total&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;

&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Shadowing a built-in by accident
&lt;/h3&gt;

&lt;p&gt;You can create a variable called &lt;code&gt;list&lt;/code&gt;, &lt;code&gt;sum&lt;/code&gt;, or &lt;code&gt;str&lt;/code&gt;. Python lets you. Python also lets you make your life harder with a smile.&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="nb"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&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="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;123&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That fails because &lt;code&gt;list&lt;/code&gt; no longer points to the built-in &lt;code&gt;list&lt;/code&gt;; it points to your variable.&lt;/p&gt;

&lt;p&gt;Avoid it. Use names like &lt;code&gt;numbers&lt;/code&gt;, &lt;code&gt;items&lt;/code&gt;, or &lt;code&gt;users&lt;/code&gt;. Your future self does not need more mystery in the workday.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key concepts from this lesson
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Scope defines where a name can be seen from.&lt;/li&gt;
&lt;li&gt;Variables created inside a function are local to that function.&lt;/li&gt;
&lt;li&gt;Variables defined outside functions live in the module's global scope.&lt;/li&gt;
&lt;li&gt;Python searches names from the inside out: local, enclosing, global, and built-in.&lt;/li&gt;
&lt;li&gt;A namespace is the map that connects names to objects.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;global&lt;/code&gt; lets you modify global variables from a function, but it should be rare.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;nonlocal&lt;/code&gt; lets you modify variables from an outer function when working with nested functions.&lt;/li&gt;
&lt;li&gt;Passing data through parameters and returning results usually creates clearer code than modifying external state.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Fix this code without using &lt;code&gt;global&lt;/code&gt;. The function should receive the current value of &lt;code&gt;total&lt;/code&gt;, add &lt;code&gt;price&lt;/code&gt;, and return the new total.&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="n"&gt;total&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;def&lt;/span&gt; &lt;span class="nf"&gt;add_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;

&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;add_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;add_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Next up: &lt;strong&gt;lambda functions&lt;/strong&gt; and higher-order functions — how to pass functions around as values, when a &lt;code&gt;lambda&lt;/code&gt; helps, and when it just turns your code into a ransom note written with symbols.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>python</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>programming</category>
    </item>
    <item>
      <title>Why We Need Databases</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Sat, 24 Oct 2026 22:41:39 +0000</pubDate>
      <link>https://dev.to/fj_palacios/why-we-need-databases-338p</link>
      <guid>https://dev.to/fj_palacios/why-we-need-databases-338p</guid>
      <description>&lt;p&gt;You start a small project. Very small. So small that saving the data in a file feels perfectly reasonable.&lt;/p&gt;

&lt;p&gt;A &lt;code&gt;users.json&lt;/code&gt; here. An &lt;code&gt;orders.csv&lt;/code&gt; there. Maybe a shared spreadsheet because “business can see it too.” Everything is calm. Everything is under control. Until someone asks: “can we see last month's orders grouped by customer, excluding cancelled ones, sorted by revenue?”&lt;/p&gt;

&lt;p&gt;And suddenly your humble file becomes a crime scene investigation: manual filters, copies named &lt;code&gt;final_v2_good_really_this_time.xlsx&lt;/code&gt;, formulas nobody wants to touch, and the very specific feeling a former coworker once summarized perfectly: “it works because God is good.”&lt;/p&gt;

&lt;p&gt;That's where databases begin.&lt;/p&gt;

&lt;p&gt;Not because they're more elegant. Not because they sound more professional. Because at some point, &lt;strong&gt;storing data&lt;/strong&gt; is not enough. You need to query it, relate it, protect it, modify it safely, and let multiple people or processes work with it at the same time without turning your application into a social experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: storing data is not managing data
&lt;/h2&gt;

&lt;p&gt;Storing data is easy.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Ada"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ada@example.com"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works. For learning, for a small script, for a demo, for that personal project that starts on a Sunday afternoon and ends up living in production for three years because “we'll clean it up later.” Spoiler: they did not clean it up.&lt;/p&gt;

&lt;p&gt;The problem doesn't show up when you have three users. It shows up when you have thousands. Or when two processes write at the same time. Or when you need to know which orders belong to which user. Or when someone deletes a line by mistake. Or when a query that used to be instant now takes long enough for you to reconsider your career choices.&lt;/p&gt;

&lt;p&gt;A flat file is not designed to answer complex questions. It's designed to store bytes. That's important, but it's only the first step.&lt;/p&gt;

&lt;p&gt;The difference between “I have data” and “I have a database” is the difference between having all the screws for a shelf in a plastic bag and having the shelf assembled, stable, and without that one leftover metal piece staring at you from the table like it knows something you don't.&lt;/p&gt;

&lt;h2&gt;
  
  
  What breaks when we use files for everything
&lt;/h2&gt;

&lt;p&gt;Files are not bad. In fact, many databases store information in files internally. The difference is that a database adds a layer of organization, rules, queries, and guarantees on top.&lt;/p&gt;

&lt;p&gt;When you use files directly, sooner or later these problems appear:&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Querying data becomes painful
&lt;/h3&gt;

&lt;p&gt;With a small file, finding something is trivial. Open the file, read line by line, find what you need.&lt;/p&gt;

&lt;p&gt;Now imagine a CSV with one million orders:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;order_id,user_id,total,status,created_at
1,42,99.90,paid,2026-07-01
2,57,19.50,cancelled,2026-07-02
3,42,149.00,paid,2026-07-03
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you want to answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;How much has each user spent?&lt;/li&gt;
&lt;li&gt;What are the top 10 best-selling products?&lt;/li&gt;
&lt;li&gt;Which orders are paid but not shipped?&lt;/li&gt;
&lt;li&gt;How much did revenue grow compared to last month?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can do it with scripts, yes. You can also assemble a shelf with a spoon if you have enough patience. Possible is not the same as reasonable.&lt;/p&gt;

&lt;p&gt;SQL exists to ask questions of your data declaratively:&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;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&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;total_spent&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&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;total_spent&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't worry about understanding every word yet. The important idea is this: instead of writing a program that walks through the file step by step, you tell the database &lt;strong&gt;what result you want&lt;/strong&gt;. The engine decides how to get it.&lt;/p&gt;

&lt;p&gt;That difference changes everything.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Relationships become manual
&lt;/h2&gt;

&lt;p&gt;Real data almost never lives alone.&lt;/p&gt;

&lt;p&gt;A user has orders. An order has line items. A line item points to a product. A product belongs to categories. A review belongs to a user and a book. Everything starts connecting to everything else, because the real world has the rude habit of not organizing itself into one pretty list.&lt;/p&gt;

&lt;p&gt;With files, you manage those relationships yourself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"product_ids"&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="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;19&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Does user &lt;code&gt;42&lt;/code&gt; exist? Do products &lt;code&gt;7&lt;/code&gt;, &lt;code&gt;12&lt;/code&gt;, and &lt;code&gt;19&lt;/code&gt; exist? What happens if product &lt;code&gt;12&lt;/code&gt; is deleted? Who prevents &lt;code&gt;user_id&lt;/code&gt; from pointing to someone who no longer exists?&lt;/p&gt;

&lt;p&gt;Exactly: you. Your code. Your discipline. Your teammate on Friday at 6:03 PM. Good luck.&lt;/p&gt;

&lt;p&gt;A relational database lets you express those rules as part of the model itself:&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;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&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;user_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Again, you don't need to master the syntax now. Keep the concept: the database can know that &lt;code&gt;orders.user_id&lt;/code&gt; must point to a real user. That's not just storage. That's integrity.&lt;/p&gt;

&lt;p&gt;And when data matters, integrity is not a fancy extra. It's the seatbelt.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Multiple writers at once is a dangerous party
&lt;/h2&gt;

&lt;p&gt;Imagine two processes updating the same file at the same time.&lt;/p&gt;

&lt;p&gt;One reads the file. The other reads it too. One writes changes. The other writes later with an older version. Result: lost data. The kind of bug that doesn't scream, doesn't explode, and doesn't leave a stack trace. It just eats information and walks away like nothing happened.&lt;/p&gt;

&lt;p&gt;That's the worst kind of bug: the one that doesn't look like a bug until you do the math.&lt;/p&gt;

&lt;p&gt;Databases are designed to handle &lt;strong&gt;concurrency&lt;/strong&gt;: multiple clients reading and writing at the same time under clear rules. That's why concepts like transactions, locks, isolation levels, and ACID exist. Today we'll only name them; later we'll open them carefully, because there is enough material there to make a spreadsheet cry.&lt;/p&gt;

&lt;p&gt;The basic idea is simple: a database doesn't just store data. It coordinates changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Security and guarantees disappear quickly
&lt;/h2&gt;

&lt;p&gt;With files, you usually have operating system permissions: who can read or write the file. That's useful, but it doesn't go far enough.&lt;/p&gt;

&lt;p&gt;In a real application, you might want rules like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The backend can insert orders, but not delete users.&lt;/li&gt;
&lt;li&gt;An analyst can read sales, but not emails.&lt;/li&gt;
&lt;li&gt;A user can only modify their own reviews.&lt;/li&gt;
&lt;li&gt;Nobody should be able to create an order with a negative total.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A database lets you model permissions, constraints, roles, validations, and access rules in a much finer-grained way. It doesn't replace application security, but it acts as your last line of defense.&lt;/p&gt;

&lt;p&gt;And yes: you want that last line. Because trusting all application code to be perfect forever is a beautiful strategy, just like trusting that “this time I will definitely remember to make a backup before touching production.”&lt;/p&gt;

&lt;h2&gt;
  
  
  So what is a database?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;database&lt;/strong&gt; is an organized system for storing, querying, modifying, and protecting persistent data.&lt;/p&gt;

&lt;p&gt;The key word is not “storing.” A file already does that. The key is the full set:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Persistence&lt;/strong&gt;: data survives when the application shuts down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure&lt;/strong&gt;: data follows an understandable model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Querying&lt;/strong&gt;: you can ask complex questions without writing the whole algorithm by hand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Relationships&lt;/strong&gt;: you can connect entities without relying on human memory and optimistic comments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integrity&lt;/strong&gt;: you can define rules to avoid invalid states.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Concurrency&lt;/strong&gt;: multiple processes can work at the same time without stepping on each other like they're editing the same Word document in 2007.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security&lt;/strong&gt;: you can control who accesses what.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Performance&lt;/strong&gt;: you can search through millions of rows without reading everything every time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Put all of that together and you no longer have “a place to put things.” You have a central piece of your system.&lt;/p&gt;

&lt;p&gt;And here's an important point: a database does not magically fix bad design. If you model your data badly, the database will obey. With admirable professionalism and terrible consequences.&lt;/p&gt;

&lt;h2&gt;
  
  
  Types of databases from a distance
&lt;/h2&gt;

&lt;p&gt;This course focuses on SQL and PostgreSQL, but it's worth knowing that not all databases use the same model.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Type&lt;/th&gt;
&lt;th&gt;How it thinks about data&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;th&gt;Where it appears&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Relational&lt;/td&gt;
&lt;td&gt;Tables, rows, columns, and relationships&lt;/td&gt;
&lt;td&gt;PostgreSQL, MySQL, SQL Server&lt;/td&gt;
&lt;td&gt;Business applications, structured data, strong integrity&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Document&lt;/td&gt;
&lt;td&gt;JSON/BSON documents&lt;/td&gt;
&lt;td&gt;MongoDB, CouchDB&lt;/td&gt;
&lt;td&gt;Flexible data, catalogs, semi-structured content&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key-value&lt;/td&gt;
&lt;td&gt;Key → value&lt;/td&gt;
&lt;td&gt;Redis, Valkey&lt;/td&gt;
&lt;td&gt;Cache, sessions, counters, simple queues&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Graph&lt;/td&gt;
&lt;td&gt;Nodes and relationships&lt;/td&gt;
&lt;td&gt;Neo4j&lt;/td&gt;
&lt;td&gt;Complex relationships: social networks, recommendations, fraud&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time-series&lt;/td&gt;
&lt;td&gt;Data indexed by time&lt;/td&gt;
&lt;td&gt;TimescaleDB, InfluxDB&lt;/td&gt;
&lt;td&gt;Metrics, sensors, events, observability&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Don't stress about memorizing this now. The goal is to give you a map, not send you out explaining distributed databases on a napkin like you're in a backend interview at 9 AM.&lt;/p&gt;

&lt;p&gt;The important idea: each type of database exists to solve different problems. But if you're learning from zero, starting with the relational model is the best investment. Many ideas about data, performance, querying, and integrity are easier to understand from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PostgreSQL
&lt;/h2&gt;

&lt;p&gt;We'll use &lt;strong&gt;PostgreSQL 18&lt;/strong&gt; throughout the course.&lt;/p&gt;

&lt;p&gt;Not because it's the only option. MySQL, MariaDB, SQL Server, Oracle, and SQLite exist and have real use cases. But PostgreSQL is an excellent database to learn with and to work with seriously because it combines several things that are hard to find together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's &lt;strong&gt;open source&lt;/strong&gt; and widely used in the industry.&lt;/li&gt;
&lt;li&gt;It implements SQL in a powerful and mostly standard way.&lt;/li&gt;
&lt;li&gt;It has advanced types: JSONB, arrays, ranges, UUIDs, full-text search.&lt;/li&gt;
&lt;li&gt;It supports constraints, transactions, views, materialized views, roles, extensions, and serious tooling.&lt;/li&gt;
&lt;li&gt;It scales from small projects to large systems.&lt;/li&gt;
&lt;li&gt;It lets you learn general SQL while also using modern features that appear in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL is a good school because it doesn't treat you like you only want to store four rows. It gives you grown-up database tools from the beginning. Some of them won't show up until much later, but it's good to know they're there, patiently waiting for you to stop writing &lt;code&gt;SELECT *&lt;/code&gt; out of habit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What learning SQL really means
&lt;/h2&gt;

&lt;p&gt;Learning SQL is not memorizing commands.&lt;/p&gt;

&lt;p&gt;Memorizing commands creates that dangerous feeling of “I know SQL” until a query appears with &lt;code&gt;LEFT JOIN&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;HAVING&lt;/code&gt;, a subquery, and a &lt;code&gt;NULL&lt;/code&gt; staring from the corner. Then the castle starts wobbling.&lt;/p&gt;

&lt;p&gt;Really learning SQL means understanding questions like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What shape does my data have?&lt;/li&gt;
&lt;li&gt;What relationship exists between these tables?&lt;/li&gt;
&lt;li&gt;Which rows am I filtering before grouping?&lt;/li&gt;
&lt;li&gt;Why is this query reading ten million rows?&lt;/li&gt;
&lt;li&gt;What index does this access pattern need?&lt;/li&gt;
&lt;li&gt;What happens if two users modify the same thing at the same time?&lt;/li&gt;
&lt;li&gt;What is my ORM actually generating?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why this course starts here, before installing anything. Because if you understand the problem, the tool stops feeling like magic. And when the tool stops feeling like magic, you start using it with judgment.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you take from this lesson
&lt;/h2&gt;

&lt;p&gt;If you understood this, you now have the mental foundation for the course:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A file can store data, but it doesn't manage querying, relationships, concurrency, and integrity well.&lt;/li&gt;
&lt;li&gt;A database adds structure, rules, guarantees, and query capabilities.&lt;/li&gt;
&lt;li&gt;SQL lets you ask for the result you want, not manually describe how to walk through the data step by step.&lt;/li&gt;
&lt;li&gt;Relational databases remain fundamental because they model structured data and relationships very well.&lt;/li&gt;
&lt;li&gt;PostgreSQL will be our main tool because it's modern, open, powerful, and widely used in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next lesson we'll set up the environment: we'll run PostgreSQL 18 with Docker, connect with &lt;code&gt;psql&lt;/code&gt; and pgAdmin, and leave a database ready for real data. If Docker still looks at you funny, &lt;a href="https://dev.to/en/courses/mastering-docker-from-scratch"&gt;Mastering Docker from Scratch&lt;/a&gt; is the perfect warm-up; we'll use it here without turning this lesson into another container-moving day.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>sql</category>
      <category>database</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Docker Compose networking: networks, services, and isolation</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Sat, 24 Oct 2026 07:46:27 +0000</pubDate>
      <link>https://dev.to/fj_palacios/docker-compose-networking-networks-services-and-isolation-5759</link>
      <guid>https://dev.to/fj_palacios/docker-compose-networking-networks-services-and-isolation-5759</guid>
      <description>&lt;p&gt;There is a very Docker Compose moment: Postgres is running, the API is running, the logs look reasonable, and the API still says it can't connect to &lt;code&gt;localhost:5432&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You check &lt;code&gt;docker compose ps&lt;/code&gt;. Postgres is &lt;code&gt;Up&lt;/code&gt;. You check the &lt;code&gt;docker-compose.yml&lt;/code&gt;. The port is there. You stare at the screen with that specific mix of suspicion and tiredness that only appears when a tool is technically telling the truth in the least helpful way possible.&lt;/p&gt;

&lt;p&gt;Is Postgres down? No. Is the port wrong? Maybe, but probably not. Is &lt;code&gt;localhost&lt;/code&gt; a trap with good branding? Exactly.&lt;/p&gt;

&lt;p&gt;In Docker Compose, each container lives in its own little network world. Once you understand that, the rest stops feeling like dark magic with YAML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compose's default network
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;docker compose up&lt;/code&gt;, Compose automatically creates a network for the project. If your directory is called &lt;code&gt;myapp&lt;/code&gt;, that network is usually named something like &lt;code&gt;myapp_default&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All services in the &lt;code&gt;docker-compose.yml&lt;/code&gt; join that network by default.&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;api&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;myapp/api&lt;/span&gt;

  &lt;span class="na"&gt;db&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;postgres:16-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though you didn't write &lt;code&gt;networks:&lt;/code&gt; anywhere, Compose did some work for you:&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 up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker network &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see a network 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;NETWORK ID     NAME            DRIVER    SCOPE
abc123def456   myapp_default   bridge    local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside that network, services can find each other by &lt;strong&gt;service name&lt;/strong&gt;. The API can connect to Postgres using &lt;code&gt;db&lt;/code&gt; as the hostname.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postgresql://myapp:secret@db:5432/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't use the container IP. Container IPs can change when services are recreated. Using container IPs in Compose is like writing down where you parked three weeks ago on a napkin: technically useful for a few minutes.&lt;/p&gt;

&lt;p&gt;Use service names.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;localhost&lt;/code&gt; inside a container is not your machine
&lt;/h2&gt;

&lt;p&gt;This is the main trap.&lt;/p&gt;

&lt;p&gt;From your machine, &lt;code&gt;localhost&lt;/code&gt; means your machine.&lt;/p&gt;

&lt;p&gt;From inside a container, &lt;code&gt;localhost&lt;/code&gt; means that container.&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;api&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;myapp/api&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://myapp:secret@localhost:5432/myapp&lt;/span&gt;

  &lt;span class="na"&gt;db&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;postgres:16-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is wrong for container-to-container communication. The API will look for Postgres inside the API container itself. Spoiler: it is not there. If it were, you'd have another problem, probably with a suspicious architecture smell.&lt;/p&gt;

&lt;p&gt;The correct version uses the service name:&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;api&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;myapp/api&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://myapp:secret@db:5432/myapp&lt;/span&gt;

  &lt;span class="na"&gt;db&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;postgres:16-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mental model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Connecting from&lt;/th&gt;
&lt;th&gt;Correct host&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Your browser to the API&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;localhost&lt;/code&gt; if the port is published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API to Postgres&lt;/td&gt;
&lt;td&gt;&lt;code&gt;db&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API to Redis&lt;/td&gt;
&lt;td&gt;&lt;code&gt;redis&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One container to another&lt;/td&gt;
&lt;td&gt;The service name&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Don't stress if this feels counterintuitive at first. &lt;code&gt;localhost&lt;/code&gt; has spent years looking innocent while ruining entire afternoons with a smile.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;ports&lt;/code&gt; is not how containers talk to each other
&lt;/h2&gt;

&lt;p&gt;Another classic confusion: thinking you need to publish a port so another container can connect to it.&lt;/p&gt;

&lt;p&gt;You don't.&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;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&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;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://myapp:secret@db:5432/myapp&lt;/span&gt;

  &lt;span class="na"&gt;db&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;postgres:16-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ports: "3000:3000"&lt;/code&gt; line publishes the API port to your machine. It lets you open &lt;code&gt;http://localhost:3000&lt;/code&gt; from your browser.&lt;/p&gt;

&lt;p&gt;But the API does not need Postgres to have &lt;code&gt;ports: "5432:5432"&lt;/code&gt; to connect to &lt;code&gt;db:5432&lt;/code&gt;. Both services are already on the internal Compose network.&lt;/p&gt;

&lt;p&gt;This matters for security. If you publish Postgres 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;db&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;postgres:16-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;5432:5432"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're exposing the database to the host. In development that can be useful if you want to connect with TablePlus, DBeaver, local &lt;code&gt;psql&lt;/code&gt;, or another external tool. But don't do it “just in case”. “Just in case” in infrastructure is a polite way of saying “I'll leave this door open and see what happens”.&lt;/p&gt;

&lt;p&gt;If only the API needs to talk to the database, don't publish the port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom networks: separate rooms
&lt;/h2&gt;

&lt;p&gt;The default network is fine for small projects. All services can see each other and life goes on.&lt;/p&gt;

&lt;p&gt;But in a more realistic application, you might have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;web&lt;/code&gt;: frontend or reverse proxy.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;api&lt;/code&gt;: backend.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;db&lt;/code&gt;: database.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;redis&lt;/code&gt;: cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no reason for &lt;code&gt;web&lt;/code&gt; to connect directly to &lt;code&gt;db&lt;/code&gt;. The frontend talks to the API. The API talks to the database. The database should not be standing in the lobby waving at the entire building.&lt;/p&gt;

&lt;p&gt;You can model that with custom networks:&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;web&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;nginx: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;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&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;api&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;myapp/api&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;frontend&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;db&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;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;networks&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="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&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;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the rules are clear:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Networks&lt;/th&gt;
&lt;th&gt;Can talk to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frontend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;frontend&lt;/code&gt;, &lt;code&gt;backend&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;web&lt;/code&gt;, &lt;code&gt;db&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;web&lt;/code&gt; and &lt;code&gt;db&lt;/code&gt; do not share a network, so they cannot resolve each other by name or connect directly. This does not replace authentication, firewalls, permissions, or common sense, but it reduces surface area. And reducing surface area is one of those boring things that prevents interesting incidents. Interesting incidents are overrated.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;expose&lt;/code&gt; vs &lt;code&gt;ports&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Compose also has &lt;code&gt;expose&lt;/code&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="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&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;myapp/api&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;expose&lt;/code&gt; documents that a service listens on a port for other services on the network, but it does not publish that port to your machine.&lt;/p&gt;

&lt;p&gt;In practice, services on the same network can connect to a container port even if you don't write &lt;code&gt;expose&lt;/code&gt;. That's why many teams don't use it. It can be useful as documentation inside the &lt;code&gt;docker-compose.yml&lt;/code&gt;, but don't confuse it with security.&lt;/p&gt;

&lt;p&gt;The important difference:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Publishes to host&lt;/th&gt;
&lt;th&gt;Main use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ports&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Access from your machine or outside&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expose&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Document internal ports&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you want to open something from the browser, use &lt;code&gt;ports&lt;/code&gt;. If only containers talk to each other, you usually don't need anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  External networks
&lt;/h2&gt;

&lt;p&gt;Sometimes you want several Compose projects to share a network. For example, a common reverse proxy (&lt;code&gt;traefik&lt;/code&gt;, &lt;code&gt;nginx&lt;/code&gt;, &lt;code&gt;caddy&lt;/code&gt;) routing multiple local apps or services on the same server.&lt;/p&gt;

&lt;p&gt;First create the network manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create shared-proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then declare it as external:&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;app&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;myapp/api&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;shared-proxy&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;shared-proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;external: true&lt;/code&gt; means: “Compose, don't create this network; it already exists, use it.”&lt;/p&gt;

&lt;p&gt;If the network doesn't exist, Compose fails. And that's good. Better to fail than silently create a new network and leave you wondering for half an hour why the proxy can't see your app. Docker already has enough material to confuse you without extra help.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network aliases
&lt;/h2&gt;

&lt;p&gt;By default, each service resolves by its name: &lt;code&gt;api&lt;/code&gt;, &lt;code&gt;db&lt;/code&gt;, &lt;code&gt;redis&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sometimes you want additional names. That's what &lt;strong&gt;aliases&lt;/strong&gt; are for.&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;api&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;myapp/api&lt;/span&gt;
    &lt;span class="na"&gt;networks&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;aliases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api-service&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal-api&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;myapp/worker&lt;/span&gt;
    &lt;span class="na"&gt;networks&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="na"&gt;networks&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From &lt;code&gt;worker&lt;/code&gt;, these hostnames point to the &lt;code&gt;api&lt;/code&gt; service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api
api-service
internal-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this carefully. An alias can help when a legacy application expects a specific hostname. But if you start adding three names per service “for convenience”, you'll end up with a tiny home-made DNS system inside YAML. Like having five nicknames for the same person in a group chat: funny until someone new tries to understand the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete example
&lt;/h2&gt;

&lt;p&gt;A reasonable stack with reverse proxy, API, database, and cache:&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;web&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;nginx: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;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&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;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://myapp:secret@db:5432/myapp&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://redis:6379&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&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;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;frontend&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;db&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;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&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;db-data:/var/lib/postgresql/data&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-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;myapp"&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;5s&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;3s&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;5&lt;/span&gt;
    &lt;span class="na"&gt;networks&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="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:7-alpine&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;5s&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;3s&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;networks&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="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&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;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice three details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only &lt;code&gt;web&lt;/code&gt; publishes a port to the host.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;api&lt;/code&gt; talks to &lt;code&gt;db&lt;/code&gt; and &lt;code&gt;redis&lt;/code&gt; using service names.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;db&lt;/code&gt; and &lt;code&gt;redis&lt;/code&gt; are not on &lt;code&gt;frontend&lt;/code&gt;, so &lt;code&gt;web&lt;/code&gt; cannot talk to them directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This does not turn your app into Fort Knox. But you're no longer putting every service in the same room with a sticky note that says “be nice”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical challenge
&lt;/h2&gt;

&lt;p&gt;Take the &lt;code&gt;docker-compose.yml&lt;/code&gt; from the previous tutorial and split it into two networks: &lt;code&gt;frontend&lt;/code&gt; and &lt;code&gt;backend&lt;/code&gt;. The API should be on both. Postgres and Redis should only be on &lt;code&gt;backend&lt;/code&gt;. If you have a web or proxy service, put it only on &lt;code&gt;frontend&lt;/code&gt; and publish only its port.&lt;/p&gt;

&lt;p&gt;Then verify that your internal URLs use service names (&lt;code&gt;db&lt;/code&gt;, &lt;code&gt;redis&lt;/code&gt;, &lt;code&gt;api&lt;/code&gt;) and not &lt;code&gt;localhost&lt;/code&gt;. If you find a &lt;code&gt;localhost&lt;/code&gt; inside an environment variable used between containers, don't look at it fondly: change it.&lt;/p&gt;




&lt;p&gt;You now have the right mental map: Compose creates a default network, services discover each other by name, &lt;code&gt;ports&lt;/code&gt; is for reaching your machine, and custom networks let you isolate what should not talk directly. In the next tutorial we'll close the Compose module with best practices: environment-specific files, secrets, resource limits, logging, and the small details that separate a useful &lt;code&gt;docker-compose.yml&lt;/code&gt; from one that looks written during a power outage.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>docker</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Docker Compose volumes and data persistence</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Fri, 23 Oct 2026 05:38:35 +0000</pubDate>
      <link>https://dev.to/fj_palacios/docker-compose-volumes-and-data-persistence-419j</link>
      <guid>https://dev.to/fj_palacios/docker-compose-volumes-and-data-persistence-419j</guid>
      <description>&lt;p&gt;Docker has a very effective teaching method: you start Postgres, insert data, stop the container, start it again, and everything is still there. Nice. You gain confidence. You clean things up. You run the wrong command. You open the app again and the database is emptier than a sprint retrospective where everyone says “all good”.&lt;/p&gt;

&lt;p&gt;Where did the data live? Inside the container? Somewhere in Docker? In a secret folder guarded by a small daemon with trust issues?&lt;/p&gt;

&lt;p&gt;Take a breath. Docker doesn't hate your data. Containers are disposable by design. If you want persistence, you need to declare it explicitly. That's what &lt;strong&gt;volumes&lt;/strong&gt; are for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: containers are not hard drives
&lt;/h2&gt;

&lt;p&gt;A container has its own filesystem. You can write files, modify data, create directories, and feel like everything is under control. The catch is that this writable layer belongs to the container.&lt;/p&gt;

&lt;p&gt;Remove the container, and those changes go with it.&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; temp-postgres &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret &lt;span class="nt"&gt;-d&lt;/span&gt; postgres:16-alpine
docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; temp-postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container is gone. Its writable changes are gone too.&lt;/p&gt;

&lt;p&gt;That's not a bug. That's the model. Containers should be safe to destroy and recreate. The image contains the application; the data lives outside.&lt;/p&gt;

&lt;p&gt;Think of a container as a moving van. Useful, necessary, maybe smelling vaguely of cheap air freshener. But you don't store your lease, family photos, and the only copy of your production database inside the van. Those go in labeled boxes.&lt;/p&gt;

&lt;p&gt;In Docker, those boxes are volumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three volume types you'll actually use
&lt;/h2&gt;

&lt;p&gt;In Compose, a service can mount several kinds of storage:&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;app&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;myapp&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;app-data:/app/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./src:/app/src&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/app/temp&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config:/app/config:ro&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;app-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those four lines are not the same thing. Treating them as interchangeable is how you end up staring at &lt;code&gt;docker volume ls&lt;/code&gt; like you're reading ancient ruins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Named volumes
&lt;/h3&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;app&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;myapp&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;app-data:/app/data&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;app-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 &lt;strong&gt;named volume&lt;/strong&gt; is storage managed by Docker. You give it a name (&lt;code&gt;app-data&lt;/code&gt;), and Docker decides where it physically lives on the host.&lt;/p&gt;

&lt;p&gt;This is the default choice for data you want to keep: databases, uploaded files, persistent caches, internal application state.&lt;/p&gt;

&lt;p&gt;The upside: it works consistently across Linux, macOS, and Windows. You don't depend on a specific host path.&lt;/p&gt;

&lt;p&gt;The downside: the files are not sitting neatly inside your project directory. Docker stores them internally. If you're like me when I started, that creates a very specific anxiety: “I know my data exists, but I don't know where it lives.” Correct. Welcome to the club.&lt;/p&gt;

&lt;p&gt;You can inspect volumes with:&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;ls
&lt;/span&gt;docker volume inspect myproject_app-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose usually prefixes volume names with the project name. If your directory is called &lt;code&gt;myproject&lt;/code&gt; and your volume is &lt;code&gt;app-data&lt;/code&gt;, the real volume might be &lt;code&gt;myproject_app-data&lt;/code&gt;. Docker isn't trying to be mysterious. It just has a gift for making simple things look slightly like infrastructure archaeology.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bind mounts
&lt;/h3&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;app&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;myapp&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;./src:/app/src&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;bind mount&lt;/strong&gt; mounts a host directory into the container. Here, &lt;code&gt;./src&lt;/code&gt; on your machine appears as &lt;code&gt;/app/src&lt;/code&gt; inside the container.&lt;/p&gt;

&lt;p&gt;This is excellent for development: edit code in your editor, the container sees the change immediately, the dev server reloads, and for one beautiful second software feels like it was designed by someone on your side.&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;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&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;./api/src:/app/src&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But bind mounts depend on the host filesystem. If &lt;code&gt;./api/src&lt;/code&gt; exists on your laptop but not on the server, deployment fails. If file synchronization is slow on macOS, you'll feel it. If someone changes the path, the container does not develop intuition.&lt;/p&gt;

&lt;p&gt;The practical rule:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Development&lt;/strong&gt;: bind mounts for source code and local configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent data&lt;/strong&gt;: named volumes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production&lt;/strong&gt;: avoid mounting source code from the host unless you have a VERY clear reason.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not “interesting”. Clear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anonymous volumes
&lt;/h3&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;app&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;myapp&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;/app/temp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;strong&gt;anonymous volume&lt;/strong&gt; has no explicit name. Docker creates one automatically for &lt;code&gt;/app/temp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This can be useful for internal container paths, but for important data it's a trap with good lighting. The volume exists, yes, but you didn't choose a stable name for it. Weeks later, you'll see a list of long IDs and have to guess which one contains what.&lt;/p&gt;

&lt;p&gt;It's like having a drawer full of unlabelled cables. One of them is definitely important. Good luck finding the right charger before USB invents another connector.&lt;/p&gt;

&lt;p&gt;For data you want to keep and understand, use named volumes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read-only volumes
&lt;/h3&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;app&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;myapp&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;./config:/app/config:ro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;:ro&lt;/code&gt; suffix mounts the volume as &lt;strong&gt;read-only&lt;/strong&gt;. The container can read &lt;code&gt;/app/config&lt;/code&gt;, but it can't write there.&lt;/p&gt;

&lt;p&gt;Use this for configuration, certificates, static files, or anything the container should not modify. If an application only needs to read a file, don't give it write access. Least privilege applies locally too. Your laptop is not a moral exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database persistence
&lt;/h2&gt;

&lt;p&gt;The most common Compose case is a database.&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;db&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;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&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;db-data:/var/lib/postgresql/data&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;db-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Postgres stores its data in &lt;code&gt;/var/lib/postgresql/data&lt;/code&gt;. By mounting &lt;code&gt;db-data&lt;/code&gt; there, the data lives in the volume, not in the container's writable layer.&lt;/p&gt;

&lt;p&gt;Now you can remove and recreate the container without losing the database:&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 down
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container changes. The volume remains.&lt;/p&gt;

&lt;p&gt;But pay attention: &lt;code&gt;down&lt;/code&gt; and &lt;code&gt;down -v&lt;/code&gt; are not polite cousins. &lt;code&gt;docker compose down&lt;/code&gt; removes containers and networks, but keeps named volumes. &lt;code&gt;docker compose down -v&lt;/code&gt; also removes the volumes declared by Compose.&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 down     &lt;span class="c"&gt;# Keeps named volumes&lt;/span&gt;
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;  &lt;span class="c"&gt;# Removes named volumes too&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;-v&lt;/code&gt; is tiny, quiet, and destructive. Like a red button without a safety cover. Use it when you deliberately want to delete data: resetting a local database, cleaning a test environment, starting from scratch.&lt;/p&gt;

&lt;p&gt;Don't use it as “general cleanup” if the data matters.&lt;/p&gt;

&lt;p&gt;For other databases, the idea is the same: mount the volume where the image stores its data.&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;postgres&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;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&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;postgres-data:/var/lib/postgresql/data&lt;/span&gt;

  &lt;span class="na"&gt;mongo&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;mongo:7&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;mongo-data:/data/db&lt;/span&gt;

  &lt;span class="na"&gt;mysql&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;mysql:8&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&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;mysql-data:/var/lib/mysql&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;postgres-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mysql-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The path changes depending on the image. The pattern doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initialization scripts
&lt;/h2&gt;

&lt;p&gt;Many official database images can run initialization scripts the first time the database is created.&lt;/p&gt;

&lt;p&gt;With Postgres, anything mounted into &lt;code&gt;/docker-entrypoint-initdb.d&lt;/code&gt; runs when the data directory is initialized:&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;db&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;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&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;db-data:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./init-scripts:/docker-entrypoint-initdb.d:ro&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;db-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&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;-- init-scripts/01-create-tables.sql&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;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&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;title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important detail: these scripts run when the data directory is empty. If the volume already exists and Postgres has already initialized, they do not run again.&lt;/p&gt;

&lt;p&gt;Feeling the “but I changed the SQL and nothing happened” moment? Good. The volume already had data. Docker is not going to wipe your database because you edited a file. That would be helpful in a deeply criminal way.&lt;/p&gt;

&lt;p&gt;To test initialization changes locally:&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 down &lt;span class="nt"&gt;-v&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That deletes the volume and forces a fresh initialization. Deliberate, visible, consequences included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development vs production
&lt;/h2&gt;

&lt;p&gt;Volumes also separate two very different needs: fast development and stable runtime behavior.&lt;/p&gt;

&lt;p&gt;In development, you want hot reload:&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="c1"&gt;# docker-compose.dev.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&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;./api&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;development&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;./api/src:/app/src&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./api/package.json:/app/package.json:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/app/node_modules&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;- /app/node_modules&lt;/code&gt; line creates an anonymous volume so the container's &lt;code&gt;node_modules&lt;/code&gt; directory doesn't get overwritten by the host. This trick exists because JavaScript decided dependencies should have more emotional mass than the actual project. We're not fixing that today.&lt;/p&gt;

&lt;p&gt;In production, you don't mount source code from your machine. You build an image and only mount the data that must persist:&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="c1"&gt;# docker-compose.prod.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&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;./api&lt;/span&gt;
      &lt;span class="na"&gt;target&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;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;api-data:/app/data&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm start&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;api-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then combine files depending on the environment:&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;# Development&lt;/span&gt;
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.yml &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.dev.yml up

&lt;span class="c"&gt;# Production&lt;/span&gt;
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.yml &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.prod.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The base &lt;code&gt;docker-compose.yml&lt;/code&gt; describes the shared stack, and the environment-specific files override what changes. Just like with &lt;a href="https://dev.to/en/courses/mastering-git-from-scratch"&gt;Git&lt;/a&gt;, you commit what describes the project and keep machine-specific details out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups: persistence is not a backup
&lt;/h2&gt;

&lt;p&gt;A persistent volume is not a backup. Say it with me: &lt;strong&gt;persistence is not backup&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A named volume survives container recreation. It does not protect you from &lt;code&gt;docker compose down -v&lt;/code&gt;, manual deletion, data corruption, an overconfident script, or you on a Friday afternoon thinking “this definitely won't break anything”.&lt;/p&gt;

&lt;p&gt;For databases, the best backup is usually the database's own tool. For Postgres:&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 &lt;span class="nb"&gt;exec &lt;/span&gt;db pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; myapp myapp &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backup.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That runs &lt;code&gt;pg_dump&lt;/code&gt; inside the &lt;code&gt;db&lt;/code&gt; container and writes the output to your machine. Clear, portable, and database-aware.&lt;/p&gt;

&lt;p&gt;You can also make a low-level copy of a volume using a temporary container:&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;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; myproject_db-data:/data:ro &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/backup &lt;span class="se"&gt;\&lt;/span&gt;
  alpine &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tar &lt;/span&gt;czf /backup/db-data-backup.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /data &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's happening here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;myproject_db-data:/data:ro&lt;/code&gt; mounts the volume read-only.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"$(pwd)":/backup&lt;/code&gt; mounts your current directory as the backup destination.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tar czf&lt;/code&gt; archives the volume contents.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--rm&lt;/code&gt; removes the temporary container when it exits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've spent years fighting shell quotes, you noticed &lt;code&gt;"$(pwd)"&lt;/code&gt;. If not, don't worry: Bash will get its own course later, because this topic deserves more than “copy this and trust me”. That's not decoration. It keeps the command from breaking when the path contains spaces. The shell is always waiting for a chance to remind you that quotes are not optional.&lt;/p&gt;

&lt;p&gt;To restore into an empty volume:&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 create myproject_db-data

docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; myproject_db-data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/backup:ro &lt;span class="se"&gt;\&lt;/span&gt;
  alpine &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tar &lt;/span&gt;xzf /backup/db-data-backup.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For databases that are actively running, don't blindly copy files while the engine is writing. It might help in development. In production, use &lt;code&gt;pg_dump&lt;/code&gt;, consistent snapshots, or the official tooling for your database. Faith can move mountains, but it does not guarantee transactional integrity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup without destroying what matters
&lt;/h2&gt;

&lt;p&gt;These Compose and Docker commands have very different effects:&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 stop
docker compose down
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;
docker volume &lt;span class="nb"&gt;ls
&lt;/span&gt;docker volume &lt;span class="nb"&gt;rm &lt;/span&gt;myproject_db-data
docker volume prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Human translation:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker compose stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stops containers, removes nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes containers and networks, keeps named volumes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker compose down -v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes containers, networks, and project volumes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker volume rm &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes one specific volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker volume prune&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes volumes not used by any container&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;docker volume prune&lt;/code&gt; asks for confirmation, but it does not know which volume was “emotionally important”. It only knows whether a container is currently using it. If you have an old volume with data you care about and no container is attached to it right now, it can be removed.&lt;/p&gt;

&lt;p&gt;Before deleting, inspect:&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 inspect myproject_db-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you're unsure, back it up first. Boring advice. So is wearing a seatbelt, until the day it stops being boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical challenge
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;docker-compose.yml&lt;/code&gt; with Postgres and a fake API or any lightweight image you know. Add a named volume for Postgres, start the stack, create some data, run &lt;code&gt;docker compose down&lt;/code&gt;, then start it again and confirm the data is still there.&lt;/p&gt;

&lt;p&gt;After that, back up the volume using the temporary &lt;code&gt;alpine&lt;/code&gt; container. You don't need an enterprise backup system yet; just understand the flow: volume → &lt;code&gt;.tar.gz&lt;/code&gt; archive → restore into an empty volume.&lt;/p&gt;




&lt;p&gt;You can now treat Docker Compose data with the respect it deserves: outside the container, with clear names, with clean separation between development and production, and with backups that don't depend on crossed fingers. In the next tutorial we'll go into Docker Compose networking: default networks, custom networks, service isolation, and why your database should not be cheerfully waving at the entire stack.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>docker</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Setting up your DevOps work environment: Linux, terminal, and essential tools</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Thu, 22 Oct 2026 06:29:04 +0000</pubDate>
      <link>https://dev.to/fj_palacios/setting-up-your-devops-work-environment-linux-terminal-and-essential-tools-ap2</link>
      <guid>https://dev.to/fj_palacios/setting-up-your-devops-work-environment-linux-terminal-and-essential-tools-ap2</guid>
      <description>&lt;p&gt;The first real collision with DevOps usually isn't Kubernetes. Or Terraform. Or that YAML file that looks like it was written by someone with strong feelings against indentation.&lt;/p&gt;

&lt;p&gt;The first collision is usually less heroic: you try to prepare your environment, open a terminal, see three different ways to “use Linux,” someone mentions SSH keys, someone else says “just use WSL,” another person says “spin up a cloud instance,” and suddenly installing a few tools feels like choosing a mortgage.&lt;/p&gt;

&lt;p&gt;Should you use local Linux? A VM? WSL? AWS? GCP? Will your laptop explode? No. Unless you run too many browser tabs while VirtualBox is trying to breathe. But conceptually, no.&lt;/p&gt;

&lt;p&gt;This lesson sets up a serious working environment for the course: real enough to learn DevOps properly, simple enough that you don't spend three days configuring the workshop before touching a single tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a DevOps learning environment actually needs
&lt;/h2&gt;

&lt;p&gt;A DevOps environment does not need to look like production from minute one. That would be like learning to drive by entering a six-lane roundabout with a truck behind you. Educational, sure. Also unnecessarily traumatic.&lt;/p&gt;

&lt;p&gt;What it does need is a place where you can practice four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;using a &lt;strong&gt;Linux terminal&lt;/strong&gt; comfortably;&lt;/li&gt;
&lt;li&gt;installing command-line tools;&lt;/li&gt;
&lt;li&gt;running services, scripts, and processes;&lt;/li&gt;
&lt;li&gt;connecting to remote machines through &lt;strong&gt;SSH&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters. DevOps lives a lot on servers. Even when we later work with containers, Kubernetes, or cloud providers, the base is still the same: a shell, permissions, processes, logs, networking, and files.&lt;/p&gt;

&lt;p&gt;If you don't have a strong terminal foundation yet, that's fine. This course starts exactly there. And if Git still feels like a black box full of branches, &lt;a href="https://dev.to/en/courses/mastering-git-from-scratch"&gt;Mastering Git from Scratch&lt;/a&gt; will save you several uncomfortable conversations with yourself.&lt;/p&gt;

&lt;p&gt;First the judgment; then the options.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recommended option: a local Ubuntu VM
&lt;/h2&gt;

&lt;p&gt;To follow this course consistently, the recommended option is a &lt;strong&gt;local VM running Ubuntu 24.04 LTS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not because Ubuntu is magically better, but because it gives us a shared baseline: real Linux, &lt;code&gt;systemd&lt;/code&gt;, &lt;code&gt;systemctl&lt;/code&gt;, &lt;code&gt;journalctl&lt;/code&gt;, networking, permissions, packages, and a machine you can break without touching your main system. Think of it as a small practice server at home. If you destroy it, delete it and create another one. Much better than learning electricity by sticking a screwdriver into the main panel.&lt;/p&gt;

&lt;p&gt;We'll use &lt;strong&gt;Multipass&lt;/strong&gt;, a Canonical tool for creating Ubuntu VMs from the CLI. The picture 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;your machine ── multipass ──&amp;gt; Ubuntu VM devops-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before continuing: Multipass is recommended because it lets us create an Ubuntu VM in a simple and reproducible way. But it is not a religious requirement.&lt;/p&gt;

&lt;p&gt;If you work on a corporate machine, have security restrictions, already use a virtualization tool approved by your company, or your computer needs a special setup, use whatever method fits your environment: VirtualBox, VMware, UTM, Hyper-V, Parallels, libvirt, or anything else you are allowed to use.&lt;/p&gt;

&lt;p&gt;The tool is not the goal. The goal is to end up with an Ubuntu 24.04 LTS VM —or an equivalent Linux distribution— that you can access, where you can install packages, run commands, manage services, and practice without touching your main system.&lt;/p&gt;

&lt;p&gt;DevOps is about understanding systems, not marrying the first installer mentioned in a tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Multipass
&lt;/h2&gt;

&lt;p&gt;Install Multipass according to your system:&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;# macOS and Linux (Homebrew)&lt;/span&gt;
brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; multipass

&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;snap &lt;span class="nb"&gt;install &lt;/span&gt;multipass

&lt;span class="c"&gt;# Arch Linux (AUR)&lt;/span&gt;
paru &lt;span class="nt"&gt;-S&lt;/span&gt; canonical-multipass
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Arch users already know the drill: if &lt;code&gt;multipassd&lt;/code&gt; won't start, install &lt;code&gt;dnsmasq&lt;/code&gt; and add &lt;code&gt;ReadWritePaths=/usr/local/etc/multipassd&lt;/code&gt; to the service. If QEMU can't find &lt;code&gt;OVMF.fd&lt;/code&gt;, symlink it from &lt;code&gt;edk2-ovmf&lt;/code&gt; to &lt;code&gt;/usr/share/qemu/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On Windows, install it from PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-e&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Canonical.Multipass&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If virtualization is disabled in your BIOS/UEFI, your company policies are strict, or Hyper-V is doing Hyper-V things, Multipass may complain. Don't stress: DevOps does not hate you personally. It's virtualization. Sometimes it feels like opening a door; sometimes it feels like getting approval from five departments to move a chair.&lt;/p&gt;

&lt;p&gt;Once installed, check that it responds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass version
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now we can create the VM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the course VM
&lt;/h2&gt;

&lt;p&gt;The course VM will be called &lt;code&gt;devops-lab&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;multipass launch 24.04 &lt;span class="nt"&gt;--name&lt;/span&gt; devops-lab &lt;span class="nt"&gt;--cpus&lt;/span&gt; 2 &lt;span class="nt"&gt;--memory&lt;/span&gt; 4G &lt;span class="nt"&gt;--disk&lt;/span&gt; 30G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command creates an Ubuntu 24.04 machine with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2 CPUs;&lt;/li&gt;
&lt;li&gt;4 GB of RAM;&lt;/li&gt;
&lt;li&gt;30 GB of disk;&lt;/li&gt;
&lt;li&gt;the name &lt;code&gt;devops-lab&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your computer is low on resources, you can reduce it to &lt;code&gt;--memory 2G&lt;/code&gt;, but 4 GB will be more comfortable for what we'll do later. A tiny VM works, sure, like a chair with one loose leg works. The question is how much trust you want to place in it.&lt;/p&gt;

&lt;p&gt;To list your VMs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To enter the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass shell devops-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Multipass already knows how to enter the machine. You don't need to configure SSH manually for everyday course work. It's a convenient door managed by the tool.&lt;/p&gt;

&lt;p&gt;To exit:&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;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To stop the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass stop devops-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To start it again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass start devops-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you break everything with educational enthusiasm:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass delete devops-lab
multipass purge
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Breaking a lab VM is not a tragedy. It's almost part of the process. Like burning the first pancake: you don't serve it, but it teaches you how hot the pan is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accepted alternatives
&lt;/h2&gt;

&lt;p&gt;Multipass will be the baseline for examples, but it is not the only valid way to follow the course.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;When to use it&lt;/th&gt;
&lt;th&gt;Main trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multipass&lt;/td&gt;
&lt;td&gt;Recommended course baseline&lt;/td&gt;
&lt;td&gt;Depends on local virtualization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WSL2&lt;/td&gt;
&lt;td&gt;You use Windows and Multipass does not fit your setup&lt;/td&gt;
&lt;td&gt;Very practical, with a few Windows moments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local Linux&lt;/td&gt;
&lt;td&gt;You already use Linux and know what you're touching&lt;/td&gt;
&lt;td&gt;You can break your real system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;You want real public infrastructure&lt;/td&gt;
&lt;td&gt;More realistic, but billing enters chat&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  WSL2 on Windows
&lt;/h3&gt;

&lt;p&gt;If you work on Windows and Multipass gives you trouble, &lt;strong&gt;WSL2&lt;/strong&gt; is a reasonable alternative. Microsoft documents the basic installation as a single command: &lt;code&gt;wsl --install&lt;/code&gt;, which installs WSL and, by default, Ubuntu.&lt;/p&gt;

&lt;p&gt;From PowerShell as administrator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--install&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart if Windows asks for it. Windows asking for a restart: the ritual that keeps the ecosystem humble.&lt;/p&gt;

&lt;p&gt;WSL2 works for many parts of the course, but it is not exactly a traditional VM. Filesystem, networking, and services can behave differently. Great to start; not always identical to a server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local Linux
&lt;/h3&gt;

&lt;p&gt;If you already use Linux, perfect. You're home. You probably also have an opinion about your distro, terminal, and package manager. Arch users already know the drill.&lt;/p&gt;

&lt;p&gt;You can follow the course on your real system, but at the beginning it helps to separate learning from your daily machine. You don't learn carpentry by removing your front door first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud later
&lt;/h3&gt;

&lt;p&gt;Cloud will show up later when we actually need it: IAM, security groups, cloud networking, Terraform against real infrastructure, managed services, and other pieces with consequences.&lt;/p&gt;

&lt;p&gt;Can free tier work? Yes, if you respect limits and conditions. Am I going to promise that the whole course will be free on cloud? No. That would be like saying “don't worry, this YAML will definitely work the first time.” Technically possible; emotionally irresponsible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Entering with Multipass and practicing SSH
&lt;/h2&gt;

&lt;p&gt;For course work, you'll usually enter like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass shell devops-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also run commands without opening a full shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass &lt;span class="nb"&gt;exec &lt;/span&gt;devops-lab &lt;span class="nt"&gt;--&lt;/span&gt; lsb_release &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;strong&gt;SSH&lt;/strong&gt; is still fundamental in DevOps. On real servers, cloud instances, and many lab environments, SSH will be your front door.&lt;/p&gt;

&lt;p&gt;So let's practice it against our local VM. First generate a key if you don't already have one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"devops-course"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accept the default path (&lt;code&gt;~/.ssh/id_ed25519&lt;/code&gt;) unless you have a reason to change it. The public part (&lt;code&gt;id_ed25519.pub&lt;/code&gt;) goes to the remote machine. The private part (&lt;code&gt;id_ed25519&lt;/code&gt;) is not shared. Ever. Think house keys: you can give the lock a copy of your public identity, but you don't post your private key in a team chat. Obvious, until it happens. And it happens.&lt;/p&gt;

&lt;p&gt;Copy the public key into the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass transfer ~/.ssh/id_ed25519.pub devops-lab:/home/ubuntu/id_ed25519.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter the VM with Multipass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass shell devops-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the VM, add the key to &lt;code&gt;authorized_keys&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="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.ssh
&lt;span class="nb"&gt;cat&lt;/span&gt; ~/id_ed25519.pub &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.ssh/authorized_keys
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 ~/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.ssh/authorized_keys
&lt;span class="nb"&gt;rm&lt;/span&gt; ~/id_ed25519.pub
&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now check the VM IP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass info devops-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for the &lt;code&gt;IPv4&lt;/code&gt; line and connect through SSH:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh ubuntu@&amp;lt;vm-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need a specific key:&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_ed25519 ubuntu@&amp;lt;vm-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this does not work on your system, that's fine: some drivers, firewalls, or networking setups may block direct access by IP. We'll use &lt;code&gt;multipass shell&lt;/code&gt; to follow the course; SSH is extra practice here, and we'll revisit it with cloud later.&lt;/p&gt;

&lt;p&gt;The important idea is this: Multipass gives you the convenient door; SSH is the door you'll use on real servers. You should know both.&lt;/p&gt;

&lt;p&gt;Now that you have a stable way into a Linux machine, you need the base tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essential tools
&lt;/h2&gt;

&lt;p&gt;We'll install the minimum reasonable toolkit for the first lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;git&lt;/code&gt;, for versioning files and scripts;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;wget&lt;/code&gt;, for making requests and downloading resources;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vim&lt;/code&gt;, because sooner or later you'll edit something in a terminal;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;htop&lt;/code&gt;, for reading processes without suffering through &lt;code&gt;top&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jq&lt;/code&gt;, for working with JSON;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;net-tools&lt;/code&gt; or modern equivalents, for basic networking commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Vim traps you and you can't exit, it's not a bug, it's an initiation ceremony. &lt;a href="https://dev.to/en/courses/mastering-vim-from-scratch"&gt;Mastering Vim from Scratch&lt;/a&gt; starts exactly from that kind of existential moment.&lt;/p&gt;

&lt;p&gt;If you're following the recommended path, enter the VM first with &lt;code&gt;multipass shell devops-lab&lt;/code&gt; and use the Ubuntu/Debian commands. If you're using another alternative, adapt the installation to your system:&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;# macOS and Linux (Homebrew)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;git curl wget vim htop jq

&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; git curl wget vim htop jq net-tools

&lt;span class="c"&gt;# Arch Linux&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-S&lt;/span&gt; git curl wget vim htop jq net-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then verify everything responds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;--version&lt;/span&gt;
curl &lt;span class="nt"&gt;--version&lt;/span&gt;
vim &lt;span class="nt"&gt;--version&lt;/span&gt;
jq &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't need to memorize every tool now. &lt;code&gt;git&lt;/code&gt; gives us traceability; &lt;code&gt;curl&lt;/code&gt; lets us talk to services; &lt;code&gt;jq&lt;/code&gt; lets us read JSON without crying; &lt;code&gt;htop&lt;/code&gt; shows processes with less of a 1998 accounting spreadsheet vibe.&lt;/p&gt;

&lt;p&gt;With tools ready, let's make the terminal more comfortable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring a comfortable terminal
&lt;/h2&gt;

&lt;p&gt;You could do the entire course with the default shell, no plugins, and no colors. You could also write all your code in a tiny-font plain text editor. Possible is not the same as wise.&lt;/p&gt;

&lt;p&gt;For now, &lt;code&gt;bash&lt;/code&gt; is fine. In fact, it's what you'll find on most servers you connect to. But this is your work terminal, your personal environment — you can afford some luxuries here. If you want a nicer experience, install &lt;code&gt;zsh&lt;/code&gt; and, optionally, Oh My Zsh. If you want the slower walkthrough, there is already a dedicated tutorial on &lt;a href="https://dev.to/en/tutorials/how-to-install-zsh-oh-my-zsh-gnu-linux"&gt;how to install ZSH and Oh My ZSH! in GNU/Linux&lt;/a&gt;, which fits this Ubuntu VM especially well.&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;# macOS and Linux (Homebrew)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;zsh

&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; zsh

&lt;span class="c"&gt;# Arch Linux&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-S&lt;/span&gt; zsh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Oh My Zsh is installed through a remote script. That's common, but it is still a remote script. The responsible version: read it before running it. The realistic version: at least run it from the official source and not from &lt;code&gt;random-devops-blog-2020.example&lt;/code&gt;, please.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing, change your default shell to &lt;code&gt;zsh&lt;/code&gt;. If you are using the recommended Multipass VM, the &lt;code&gt;ubuntu&lt;/code&gt; user has no password, so use &lt;code&gt;sudo&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="nb"&gt;sudo &lt;/span&gt;chsh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;which zsh&lt;span class="si"&gt;)&lt;/span&gt; ubuntu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On other systems, &lt;code&gt;chsh&lt;/code&gt; may ask for your password without &lt;code&gt;sudo&lt;/code&gt;. Both achieve the same result.&lt;/p&gt;

&lt;p&gt;Now add a few useful aliases to the end of &lt;code&gt;~/.zshrc&lt;/code&gt; or &lt;code&gt;~/.bashrc&lt;/code&gt;, depending on your shell:&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;# DevOps aliases&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;ll&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ls -lah'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;ports&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'sudo ss -tulpn'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;logs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'sudo journalctl -f'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;c&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'clear'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload the configuration:&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;source&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if you use Bash:&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;source&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't fill your shell with 200 aliases on day one. That looks like productivity, but it's often decoration with side effects. Start with four or five you actually use. Your terminal should help you, not become an airplane cockpit where you don't know which button turns off the engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the course workspace
&lt;/h2&gt;

&lt;p&gt;Let's leave a clean structure for the next exercises:&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;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/devops-course/&lt;span class="o"&gt;{&lt;/span&gt;scripts,configs,logs,projects,notes&lt;span class="o"&gt;}&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ~/devops-course

&lt;span class="nb"&gt;touch &lt;/span&gt;notes/lesson-02.md
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'DevOps course workspace ready\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; notes/lesson-02.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the result (using the &lt;code&gt;ll&lt;/code&gt; alias you just created):&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;You should see something 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;drwxr-xr-x  7 user user 4.0K Jul  6 10:00 .
drwxr-x--- 20 user user 4.0K Jul  6 10:00 ..
drwxr-xr-x  2 user user 4.0K Jul  6 10:00 configs
drwxr-xr-x  2 user user 4.0K Jul  6 10:00 logs
drwxr-xr-x  2 user user 4.0K Jul  6 10:00 notes
drwxr-xr-x  2 user user 4.0K Jul  6 10:00 projects
drwxr-xr-x  2 user user 4.0K Jul  6 10:00 scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now initialize a Git repository to version your notes and scripts:&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 status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;On branch main

No commits yet

Untracked files:
  notes/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make the first commit. First, tell Git who you are (if this is your first time using Git on this machine):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config user.email &lt;span class="s2"&gt;"you@email.com"&lt;/span&gt;
git config user.name &lt;span class="s2"&gt;"Your Name"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;--global&lt;/code&gt; if you want this configuration applied to all your repositories.&lt;/p&gt;

&lt;p&gt;Now for the actual commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add notes/lesson-02.md
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: initialize devops course workspace"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, even for a learning workspace. Especially for a learning workspace. If you break something, Git gives you a way back without doing archaeology in your own directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final check
&lt;/h2&gt;

&lt;p&gt;Before closing, run this small checklist:&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;pwd
&lt;/span&gt;git &lt;span class="nt"&gt;--version&lt;/span&gt;
ssh &lt;span class="nt"&gt;-V&lt;/span&gt;
curl &lt;span class="nt"&gt;--version&lt;/span&gt;
jq &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And confirm mentally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I have a usable Linux terminal;&lt;/li&gt;
&lt;li&gt;I have a course directory;&lt;/li&gt;
&lt;li&gt;I can install tools;&lt;/li&gt;
&lt;li&gt;Git works;&lt;/li&gt;
&lt;li&gt;I know how to connect through SSH, or at least I understand what I need to do it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of this fails, don't continue as if nothing happened. That's one of the big DevOps lessons: a half-broken environment today becomes a lost afternoon tomorrow. Like a chair with one loose leg: you can sit on it, yes, but part of your brain is already drafting the incident report.&lt;/p&gt;




&lt;p&gt;Your environment is no longer “an open terminal where things happen.” You now have a minimal workshop: a shell, base tools, a working structure, Git, and a clear idea of how to enter a remote machine.&lt;/p&gt;

&lt;p&gt;In the next lesson we'll go down into the Linux filesystem: directories, files, permissions, and basic commands. That's where &lt;code&gt;/etc&lt;/code&gt;, &lt;code&gt;/var&lt;/code&gt;, &lt;code&gt;/home&lt;/code&gt;, and friends stop looking like names chosen by someone in a hurry.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Introduction to the DevOps world: culture, automation, and feedback</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Wed, 21 Oct 2026 06:50:40 +0000</pubDate>
      <link>https://dev.to/fj_palacios/introduction-to-the-devops-world-culture-automation-and-feedback-j14</link>
      <guid>https://dev.to/fj_palacios/introduction-to-the-devops-world-culture-automation-and-feedback-j14</guid>
      <description>&lt;p&gt;It's 5:43 PM on a Thursday. Development has just finished a feature. Operations needs to deploy it. The feature works locally, tests are green, the ticket is Done, and someone is already thinking about closing the laptop with dignity.&lt;/p&gt;

&lt;p&gt;Then production shows up and says: “adorable, but no.”&lt;/p&gt;

&lt;p&gt;The server has a different Node version. The environment variable is missing. The deploy script depends on a manual step known by exactly one person. Logs live on one machine, configuration on another, and the document explaining the process is called &lt;code&gt;deploy-final-v2-good-now-for-real.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Whose fault is it? Dev? Ops? The VPN? That &lt;code&gt;README&lt;/code&gt; nobody has updated since 2021? If you're just starting out, this may sound exaggerated. If you've worked in software before, you probably just remembered a specific face. Sorry about that.&lt;/p&gt;

&lt;p&gt;Welcome to the DevOps world.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem before DevOps
&lt;/h2&gt;

&lt;p&gt;For a long time, software delivery was organized around a very clear separation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Development&lt;/strong&gt; wrote the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operations&lt;/strong&gt; managed servers, networks, deployments, and production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On paper, that sounds reasonable. Like separating kitchen and service in a restaurant: one team prepares the food, another serves it. The problem starts when the kitchen doesn't know how the dish is served, service doesn't know what's in the dish, and the customer is staring at lasagna that's still frozen in the middle.&lt;/p&gt;

&lt;p&gt;In software, that separation produced familiar symptoms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“It works on my machine.”&lt;/li&gt;
&lt;li&gt;“Ops handles deployments, ask them.”&lt;/li&gt;
&lt;li&gt;“We can't deploy until Friday because there's a change window.”&lt;/li&gt;
&lt;li&gt;“Where are the logs?”&lt;/li&gt;
&lt;li&gt;“Who touched production?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;“It works on my machine” is practically cultural heritage in this industry. It solves nothing technically, but it does buy three seconds of awkward silence. Like assembling IKEA furniture, finding one spare metal part, and deciding it was probably decorative. The shelf is standing. For now.&lt;/p&gt;

&lt;p&gt;The real problem wasn't that Dev was irresponsible or Ops was slow. That caricature is convenient, but wrong. The problem was &lt;strong&gt;the system of work&lt;/strong&gt;: separated teams, different goals, little automation, late feedback, and deployments treated as special events instead of a normal part of development.&lt;/p&gt;

&lt;p&gt;That's where DevOps comes in.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;DevOps&lt;/strong&gt; is a culture and a set of practices for delivering software faster, more safely, and more reliably through collaboration, automation, and continuous feedback.&lt;/p&gt;

&lt;p&gt;The important sentence is this: &lt;strong&gt;DevOps is not a tool&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is not Jenkins. It is not Docker. It is not Kubernetes. It is not Terraform. It is not having a 400-line YAML file and hoping that if you look at it respectfully enough, it won't fail.&lt;/p&gt;

&lt;p&gt;Tools help, of course. But DevOps starts earlier: it starts when development and operations stop working like two departments throwing packages over a fence and start sharing responsibility for the whole software lifecycle.&lt;/p&gt;

&lt;p&gt;That full lifecycle looks roughly 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;Plan → Code → Build → Test → Release → Deploy → Operate → Monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DevOps tries to make that cycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;faster&lt;/strong&gt;, because repetitive steps are automated;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;safer&lt;/strong&gt;, because manual changes and surprises are reduced;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;more observable&lt;/strong&gt;, because you know what is happening when something breaks;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;more collaborative&lt;/strong&gt;, because the team shares context and responsibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The uncomfortable bit: DevOps cannot be “installed.” It has to be practiced. If a company says “we bought DevOps,” what it probably bought is a tool, consulting, or a laptop sticker. Real DevOps changes how work happens.&lt;/p&gt;

&lt;p&gt;And to change how work happens, you need three core ideas.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three core ideas: collaboration, automation, and feedback
&lt;/h2&gt;

&lt;p&gt;We could build a giant list of principles, but for now keep three. They are the tripod. If one leg fails, the camera falls and the picture comes out blurry. Humble metaphor, accurate result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Collaboration
&lt;/h3&gt;

&lt;p&gt;Dev and Ops should not meet for the first time on deployment day.&lt;/p&gt;

&lt;p&gt;Collaboration means that people writing the code understand how it runs in production, and people operating the system understand what is changing in the product. Not so everyone does everything all the time — that ends in calendar-shaped chaos — but so there are no absurd walls.&lt;/p&gt;

&lt;p&gt;A developer doesn't need to become a BGP networking expert to deploy an API. But they should understand what variables it needs, what ports it exposes, what logs it emits, and how to tell if it is healthy.&lt;/p&gt;

&lt;p&gt;An operations person doesn't need to know every detail of the business domain. But they should be involved in decisions that affect deployment, capacity, observability, and recovery.&lt;/p&gt;

&lt;p&gt;DevOps removes the “I already did my part” escape hatch. In real systems, that sentence is a trap. Your part does not end when you run &lt;code&gt;git push&lt;/code&gt;; it ends when the software delivers value in a real environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automation
&lt;/h3&gt;

&lt;p&gt;Every repeated manual step is a future incident updating its résumé.&lt;/p&gt;

&lt;p&gt;Building by hand, copying files over SSH, restarting services “in the correct order,” editing configuration directly in production... all of that can work once. You can also cross a road with your eyes closed once. That doesn't make it a methodology.&lt;/p&gt;

&lt;p&gt;Automation turns fragile rituals into repeatable steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Code → pipeline → tests → build → deploy → monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of depending on someone remembering the exact ceremony, the process lives in scripts, pipelines, repositories, and infrastructure as code.&lt;/p&gt;

&lt;p&gt;This is where tools we'll cover later enter the picture: Git, Jenkins, Docker, Kubernetes, Terraform, Prometheus, Grafana. But notice the mental order: &lt;strong&gt;problem first, tool second&lt;/strong&gt;. Buying Kubernetes before understanding deployment is like buying an excavator because you want to hang a picture frame.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feedback
&lt;/h3&gt;

&lt;p&gt;Fast feedback prevents errors from arriving late, expensive, and dressed as production.&lt;/p&gt;

&lt;p&gt;Automated tests, pipelines, alerts, logs, metrics, dashboards, postmortems: all of this exists to answer questions as early as possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the change work?&lt;/li&gt;
&lt;li&gt;Is the system still healthy?&lt;/li&gt;
&lt;li&gt;Did the deploy break something?&lt;/li&gt;
&lt;li&gt;Are users suffering?&lt;/li&gt;
&lt;li&gt;Can we recover quickly?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without feedback, you work blind. And working blind in infrastructure has a technical name: Friday afternoon.&lt;/p&gt;

&lt;p&gt;With collaboration, automation, and feedback, you can already see why DevOps is not “put someone in charge of Jenkins.” It is a way to reduce distance between writing software and operating it.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, is DevOps a role?
&lt;/h2&gt;

&lt;p&gt;Here is where the confusion gets fun. Fun in the sense of “some org charts look generated by a sleep-deprived AI.”&lt;/p&gt;

&lt;p&gt;DevOps started as culture and practices, not a job title. Then the industry did what the industry does: it turned the idea into job postings.&lt;/p&gt;

&lt;p&gt;Today you'll see titles like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DevOps Engineer&lt;/li&gt;
&lt;li&gt;Site Reliability Engineer&lt;/li&gt;
&lt;li&gt;Platform Engineer&lt;/li&gt;
&lt;li&gt;Cloud Engineer&lt;/li&gt;
&lt;li&gt;Infrastructure Engineer&lt;/li&gt;
&lt;li&gt;Build and Release Engineer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Are they the same? No. Do they overlap? A lot. Does it depend on the company? Absolutely. Does that help beginners? Not even slightly.&lt;/p&gt;

&lt;p&gt;A practical way to frame it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Focus&lt;/th&gt;
&lt;th&gt;Main question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DevOps&lt;/td&gt;
&lt;td&gt;How do we deliver software better and with less friction?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SRE&lt;/td&gt;
&lt;td&gt;How do we make the system reliable in a measurable way?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform Engineering&lt;/td&gt;
&lt;td&gt;How do we build a platform so other teams deliver software better?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;DevOps usually focuses on collaboration, automation, CI/CD, and infrastructure. SRE takes many of those ideas into operational reliability: SLOs, error budgets, incident response, toil, postmortems. Platform Engineering climbs another level: internal tools, Golden Paths, and self-service so teams don't reinvent deployment in every repository.&lt;/p&gt;

&lt;p&gt;If DevOps reduces the wall between Dev and Ops, SRE puts numbers on reliability, and Platform Engineering builds the road teams drive on. If the road is well built, nobody thinks about it. If it isn't, everyone ends up in an infinite roundabout holding a Jira ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a traditional DevOps Engineer does
&lt;/h2&gt;

&lt;p&gt;In many companies, the DevOps Engineer role combines several responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Designing and maintaining &lt;strong&gt;CI/CD pipelines&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Automating &lt;strong&gt;deployments&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Managing &lt;strong&gt;infrastructure as code&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Maintaining servers, environments, and configuration.&lt;/li&gt;
&lt;li&gt;Setting up basic monitoring.&lt;/li&gt;
&lt;li&gt;Helping teams deploy more safely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means touching many moving parts. Some pleasant. Some YAML-shaped.&lt;/p&gt;

&lt;p&gt;The usual skills include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux/Unix&lt;/strong&gt;, because most servers live there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terminal and scripting&lt;/strong&gt;, especially Bash and sometimes Python.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git&lt;/strong&gt;, because without version control there is no traceability. If Git still feels like a black box with branches, &lt;a href="https://dev.to/en/courses/mastering-git-from-scratch"&gt;Mastering Git from Scratch&lt;/a&gt; will save you real pain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containers&lt;/strong&gt;, usually Docker. If you want to enter there before Kubernetes, &lt;a href="https://dev.to/en/courses/mastering-docker-from-scratch"&gt;Mastering Docker from Scratch&lt;/a&gt; is the natural path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud providers&lt;/strong&gt;, like AWS or GCP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;, so you can see what is happening when the system stops smiling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is not knowing all of this on day one. No serious person expects that. The key is understanding the map: which piece solves which problem and how the pieces talk to each other.&lt;/p&gt;

&lt;p&gt;Because if you don't have a map, every tool looks like a universal solution. That's how you end up trying to fix a DNS problem with Terraform. Don't ask how I know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DevOps ecosystem
&lt;/h2&gt;

&lt;p&gt;A simple way to understand the ecosystem is to follow a change from the moment it is written to the moment it is observed in production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Development → Build → Test → Deploy → Monitor

Git → Jenkins → Docker → Kubernetes → Prometheus
      GitHub    Registry   Helm         Grafana
      GitLab    Artifacts  ArgoCD       Alerts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't memorize all the names yet. Seriously, don't stress. The list looks like a hardware store where someone mixed screwdrivers, saws, cables, and a welding machine in the same aisle.&lt;/p&gt;

&lt;p&gt;What matters is understanding the categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version control&lt;/strong&gt;: where code and change history live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD&lt;/strong&gt;: how build, tests, and deploy are automated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Packaging&lt;/strong&gt;: how the application is packaged to run consistently across environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration&lt;/strong&gt;: how multiple services and their lifecycle are managed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure as Code&lt;/strong&gt;: how infrastructure is described in a versioned way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;: how you see what is happening inside the system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each category answers a different question. That distinction saves you from a classic trap: learning tools without understanding the problem. That produces long résumés and fragile systems. Lots of technology, little judgment. Like buying a professional toolbox to tighten one screw and ending up disassembling the washing machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The course roadmap
&lt;/h2&gt;

&lt;p&gt;This course is designed so the map appears gradually, not as a wall of acronyms on day one.&lt;/p&gt;

&lt;p&gt;We'll move through three large phases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Traditional DevOps
&lt;/h3&gt;

&lt;p&gt;First we'll build fundamentals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux and terminal.&lt;/li&gt;
&lt;li&gt;Processes, services, logs, and basic networking.&lt;/li&gt;
&lt;li&gt;Bash scripting.&lt;/li&gt;
&lt;li&gt;Git applied to infrastructure.&lt;/li&gt;
&lt;li&gt;CI/CD with Jenkins.&lt;/li&gt;
&lt;li&gt;Docker and containers.&lt;/li&gt;
&lt;li&gt;Kubernetes.&lt;/li&gt;
&lt;li&gt;Terraform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This phase answers the question: &lt;strong&gt;how do I automate and operate the software delivery lifecycle?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here you'll get your hands dirty. Good. DevOps without touching a terminal is like learning to swim from a PowerPoint deck.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: SRE
&lt;/h3&gt;

&lt;p&gt;Then we'll move into Site Reliability Engineering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Advanced observability.&lt;/li&gt;
&lt;li&gt;SLOs and error budgets.&lt;/li&gt;
&lt;li&gt;Incident management.&lt;/li&gt;
&lt;li&gt;Blameless postmortems.&lt;/li&gt;
&lt;li&gt;Chaos engineering.&lt;/li&gt;
&lt;li&gt;Capacity planning.&lt;/li&gt;
&lt;li&gt;Sustainable on-call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This phase asks a different question: &lt;strong&gt;how do I make the system reliable without turning the team into a permanent fire brigade?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SRE introduces a fundamental idea: reliability with numbers. Not “we want it to work well,” but “this service must meet this SLO, and this is the acceptable error margin.” Less heroic intuition, more engineering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: Platform Engineering
&lt;/h3&gt;

&lt;p&gt;Finally we reach Platform Engineering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Golden Paths.&lt;/li&gt;
&lt;li&gt;Developer Experience.&lt;/li&gt;
&lt;li&gt;Self-service.&lt;/li&gt;
&lt;li&gt;Backstage.&lt;/li&gt;
&lt;li&gt;Service catalog.&lt;/li&gt;
&lt;li&gt;Internal Developer Platforms.&lt;/li&gt;
&lt;li&gt;Internal tooling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This phase answers the mature question: &lt;strong&gt;how do I build a platform so other teams deliver software better without depending on me for every step?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That shift is huge. You move from putting out fires to designing systems that prevent fires from starting. Or, more realistically, systems that at least keep the fire in a controlled area, with alerts, a runbook, and an extinguisher that didn't expire in 2019.&lt;/p&gt;

&lt;h2&gt;
  
  
  What you should take from this first lesson
&lt;/h2&gt;

&lt;p&gt;DevOps is not about tools. It is about reducing friction between building software and operating it in a real environment.&lt;/p&gt;

&lt;p&gt;Tools matter, but they are the consequence. Git, Jenkins, Docker, Kubernetes, Terraform, Prometheus, or Backstage make sense when you understand what problem they solve. Without that context, they are just expensive names in an architecture nobody wants to touch.&lt;/p&gt;

&lt;p&gt;Keep these ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DevOps emerged to break silos between Development and Operations.&lt;/li&gt;
&lt;li&gt;The foundation is collaboration, automation, and fast feedback.&lt;/li&gt;
&lt;li&gt;DevOps is not a pure role, even though roles called DevOps Engineer exist.&lt;/li&gt;
&lt;li&gt;SRE and Platform Engineering are related evolutions, not magic words.&lt;/li&gt;
&lt;li&gt;The course will move from real fundamentals to modern internal platforms, without jumping straight into deep YAML.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Think of a real or imaginary project you know. Write a short note describing how a change is delivered today: who writes the code, how it is tested, how it is deployed, where logs are checked, and how the team knows it works. If at any point your answer is “I don't know” or “someone does it manually,” perfect: you just found a future lesson in this course.&lt;/p&gt;

&lt;p&gt;In the next tutorial we'll prepare the working environment. We'll compare local virtual machines, cloud environments, and WSL2, then set up a Linux base so you can start using the terminal without relying on dark magic or “it was already configured on my laptop.”&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Marks and jumps in Vim: navigate without losing the thread</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:31:21 +0000</pubDate>
      <link>https://dev.to/fj_palacios/marks-and-jumps-in-vim-navigate-without-losing-the-thread-2a1p</link>
      <guid>https://dev.to/fj_palacios/marks-and-jumps-in-vim-navigate-without-losing-the-thread-2a1p</guid>
      <description>&lt;p&gt;You're editing a function at line 347. Before finishing, you need to check how another function works. You jump with &lt;code&gt;/&lt;/code&gt;, get there, read it, realize you also need to see a constant you defined near the top. You head up there. When you try to get back to line 347 — you can't find it. You search for the function name, land close, move around a bit. You pick up the thread again. Now you need to check one more thing in another file.&lt;/p&gt;

&lt;p&gt;This isn't editing anymore. This is sightseeing.&lt;/p&gt;

&lt;p&gt;Vim has a full system for remembering where you've been: marks for manually saving positions, a jump list for the automatic history of your movements, and a changelist for the spots where you made edits. Once you work these into your flow, "where was I?" stops being a question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Marks: leaving a signal before you go
&lt;/h2&gt;

&lt;p&gt;A mark is a saved position. To create one, use &lt;code&gt;m&lt;/code&gt; followed by a letter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="k"&gt;ma&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That saves your current position under the key &lt;code&gt;a&lt;/code&gt;. You can use any lowercase letter (a-z). Now move wherever you need to — another function, another section, another file. To return exactly where you were, use backtick followed by the letter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;`&lt;span class="k"&gt;a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cursor back to that exact line and column.&lt;/p&gt;

&lt;p&gt;There's also the apostrophe version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;'&lt;span class="k"&gt;a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference: &lt;code&gt;`a&lt;/code&gt; (backtick) jumps to the exact position — line &lt;strong&gt;and&lt;/strong&gt; column. &lt;code&gt;'a&lt;/code&gt; (apostrophe) jumps to the line, but to the first non-blank character. It's the difference between remembering "it was in the third drawer, under the old batteries, in the right corner" versus just "the third drawer." Both work. One is more precise.&lt;/p&gt;

&lt;p&gt;Which one to use? Backtick. Almost always. Unless you specifically need the first character of the line, backtick puts you back exactly where you left the cursor.&lt;/p&gt;

&lt;p&gt;Marks are movements too, which means they compose with &lt;a href="https://dev.to/en/tutorials/normal-mode-power-of-vim"&gt;Normal mode grammar&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="k"&gt;d&lt;/span&gt;`&lt;span class="k"&gt;a&lt;/span&gt;    " &lt;span class="k"&gt;delete&lt;/span&gt; from &lt;span class="nb"&gt;cursor&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="k"&gt;mark&lt;/span&gt; &lt;span class="k"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;y&lt;/span&gt;'&lt;span class="k"&gt;b&lt;/span&gt;    " &lt;span class="k"&gt;yank&lt;/span&gt; from &lt;span class="nb"&gt;cursor&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; the &lt;span class="nb"&gt;line&lt;/span&gt; of &lt;span class="k"&gt;mark&lt;/span&gt; &lt;span class="k"&gt;b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vim gives you 26 local marks (a-z) and 26 global ones (A-Z) — fifty-two total. Exactly the number that makes you think you need a naming convention and maybe a spreadsheet to track them all. What you'll actually use is two or three: &lt;code&gt;ma&lt;/code&gt; to mark where you jumped from, &lt;code&gt;mb&lt;/code&gt; for something you need to check later, and &lt;code&gt;ms&lt;/code&gt; for that other thing you've been putting off. Always the same two or three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Global marks: across files
&lt;/h2&gt;

&lt;p&gt;Uppercase letters (A-Z) are &lt;strong&gt;global&lt;/strong&gt; marks: they work across files and persist between Vim sessions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;mA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That saves the current position under &lt;code&gt;A&lt;/code&gt;. Tomorrow, after closing and reopening Vim, &lt;code&gt;`A&lt;/code&gt; takes you right back there — opening the file if needed.&lt;/p&gt;

&lt;p&gt;Useful when you're constantly jumping between two or three files: &lt;code&gt;mA&lt;/code&gt; in the config file, &lt;code&gt;mB&lt;/code&gt; in the routes file, &lt;code&gt;mC&lt;/code&gt; in the main entry point. You jump between them without searching for anything.&lt;/p&gt;

&lt;p&gt;Honest limitation: only one slot per letter. If you use &lt;code&gt;mA&lt;/code&gt; in a different file, the previous &lt;code&gt;A&lt;/code&gt; is gone. Fine with three files; not the right tool if you're trying to build a full project index with 26 active entries. For navigating many files, Vim has better tools you'll see later in the course.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic marks: the ones Vim sets for you
&lt;/h2&gt;

&lt;p&gt;Vim also saves certain positions automatically. You don't have to do anything — they're already there when you need them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mark&lt;/th&gt;
&lt;th&gt;What it stores&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;`.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Position of the last change in the file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;`^&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Position where you left Insert mode last time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;`[&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start of the last yanked or changed text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;`]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;End of the last yanked or changed text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;`&amp;lt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start of the last visual selection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;`&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;End of the last visual selection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most useful one day-to-day: &lt;code&gt;`.&lt;/code&gt; jumps to the last place you made a change. No setup, no thinking. &lt;code&gt;`.&lt;/code&gt; and you're there.&lt;/p&gt;

&lt;p&gt;Then there's &lt;code&gt;''&lt;/code&gt; (two apostrophes), or &lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt; (two backticks) for the exact-position version. Both store where you were &lt;strong&gt;before your last big jump&lt;/strong&gt; — a search, &lt;code&gt;gg&lt;/code&gt;, &lt;code&gt;G&lt;/code&gt;, jumping to a mark. It's the retroactive "take me back to the start" command. You don't activate it in advance; Vim records it automatically on every large jump.&lt;/p&gt;

&lt;p&gt;To see all active marks at any point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;marks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It shows a table with name, line, column, and the text at that line. Useful when you've been working for a while and can't remember which mark points where — which is exactly when you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The jump list: the editor's history
&lt;/h2&gt;

&lt;p&gt;Beyond manual marks, Vim keeps an automatic position history called the &lt;strong&gt;jump list&lt;/strong&gt;. Every large jump — searching with &lt;code&gt;/&lt;/code&gt;, going to a mark, &lt;code&gt;gg&lt;/code&gt;, &lt;code&gt;G&lt;/code&gt;, jumping to a definition — gets recorded.&lt;/p&gt;

&lt;p&gt;To navigate that history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;Ctrl&lt;span class="p"&gt;+&lt;/span&gt;&lt;span class="k"&gt;o&lt;/span&gt;    " &lt;span class="k"&gt;go&lt;/span&gt; back &lt;span class="p"&gt;(&lt;/span&gt;older jump&lt;span class="p"&gt;)&lt;/span&gt;
Ctrl&lt;span class="p"&gt;+&lt;/span&gt;&lt;span class="k"&gt;i&lt;/span&gt;    " &lt;span class="k"&gt;go&lt;/span&gt; forward &lt;span class="p"&gt;(&lt;/span&gt;newer jump&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Ctrl+o&lt;/code&gt; is your browser's back button, but for positions inside the editor. If you've made four jumps from where you started, four &lt;code&gt;Ctrl+o&lt;/code&gt; presses bring you back one step at a time. &lt;code&gt;Ctrl+i&lt;/code&gt; goes the other way.&lt;/p&gt;

&lt;p&gt;To see the full list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;jumps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The jump list holds 100 positions. A hundred. Just enough to make it look like something you need to actively manage — you don't. When it fills up, the oldest entry quietly disappears. Vim doesn't send a notification. You keep editing.&lt;/p&gt;

&lt;p&gt;A note about &lt;a href="https://dev.to/en/tutorials/search-and-find-vim"&gt;searches with &lt;code&gt;/&lt;/code&gt; and &lt;code&gt;?&lt;/code&gt;&lt;/a&gt;: every jump to a result registers in the jump list. If you move through six matches with &lt;code&gt;n&lt;/code&gt;, you have six entries. &lt;code&gt;Ctrl+o&lt;/code&gt; undoes the trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The changelist: where you edited
&lt;/h2&gt;

&lt;p&gt;The jump list tracks positions. The &lt;strong&gt;changelist&lt;/strong&gt; tracks specifically the spots where you made a change in the file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="k"&gt;g&lt;/span&gt;;    " &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; older &lt;span class="k"&gt;change&lt;/span&gt;
&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    " &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; newer &lt;span class="k"&gt;change&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see the full list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;changes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The common case: you edited something, jumped to another spot to check a variable, and now you want to get back exactly to where you were typing. &lt;code&gt;g;&lt;/code&gt; and you're there.&lt;/p&gt;

&lt;p&gt;The changelist stores the last 100 changes with exact positions — a detailed audit trail of everything you've touched in the file since you opened it. In practice, you'll use &lt;code&gt;g;&lt;/code&gt; once, to get back to the last place you typed something. That's what it's there for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Special jumps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;%&lt;/code&gt;: the matching pair
&lt;/h3&gt;

&lt;p&gt;Place the cursor on a parenthesis, brace, or bracket, and press &lt;code&gt;%&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It jumps to the matching character. On &lt;code&gt;(&lt;/code&gt; → jumps to &lt;code&gt;)&lt;/code&gt;. On &lt;code&gt;}&lt;/code&gt; → jumps to the &lt;code&gt;{&lt;/code&gt; that opens it. You can bounce back and forth with repeated &lt;code&gt;%&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Useful in heavily nested code: instead of visually counting where a block ends, &lt;code&gt;%&lt;/code&gt; takes you there directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;gd&lt;/code&gt; and &lt;code&gt;gD&lt;/code&gt;: go to definition
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="nb"&gt;gd&lt;/span&gt;    " &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; local definition &lt;span class="p"&gt;(&lt;/span&gt;within the current scope&lt;span class="p"&gt;)&lt;/span&gt;
gD    " &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; global definition &lt;span class="p"&gt;(&lt;/span&gt;from the &lt;span class="nb"&gt;top&lt;/span&gt; of the &lt;span class="k"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gd&lt;/code&gt; finds the first occurrence of the word under the cursor within the local scope — useful for jumping to where a variable is declared. &lt;code&gt;gD&lt;/code&gt; does the same but searches from the beginning of the file.&lt;/p&gt;

&lt;p&gt;Not the same as LSP, which you'll see later in the course and which jumps with actual semantic context. &lt;code&gt;gd&lt;/code&gt; and &lt;code&gt;gD&lt;/code&gt; are text searches. In files without LSP, or for local variables with clear names, they work without any configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;gf&lt;/code&gt;: open the file under the cursor
&lt;/h3&gt;

&lt;p&gt;The most underrated command in this lesson.&lt;/p&gt;

&lt;p&gt;Place your cursor over a file path in the text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../config/app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./utils/format&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And press:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;gf
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vim opens the file. No extra commands, no file explorer, no copying the path and pasting it into &lt;code&gt;:e&lt;/code&gt;. If it exists, &lt;code&gt;gf&lt;/code&gt; goes there. To come back, use &lt;code&gt;Ctrl+o&lt;/code&gt; — the jump was already recorded in the jump list.&lt;/p&gt;

&lt;p&gt;One caveat: &lt;code&gt;gf&lt;/code&gt; works with the literal text under the cursor. Paths using bundler aliases (&lt;code&gt;@/components&lt;/code&gt;) or implicit extensions might need extra configuration to resolve. On explicit paths, it works out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key concepts from this lesson
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;m{a-z}&lt;/code&gt; creates a local mark; &lt;code&gt;`{letter}&lt;/code&gt; jumps to the exact line and column; &lt;code&gt;'{letter}&lt;/code&gt; jumps to the line&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;m{A-Z}&lt;/code&gt; creates a global mark that persists across sessions and works across files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;`.&lt;/code&gt; jumps to the last change; &lt;code&gt;''&lt;/code&gt; jumps to the position before the last big jump&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:marks&lt;/code&gt; lists all active marks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Ctrl+o&lt;/code&gt; / &lt;code&gt;Ctrl+i&lt;/code&gt; move backward and forward through the jump list&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;g;&lt;/code&gt; / &lt;code&gt;g,&lt;/code&gt; move through the changelist (edit history)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;%&lt;/code&gt; jumps between matching open and close characters&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gd&lt;/code&gt; / &lt;code&gt;gD&lt;/code&gt; jump to the local or global first occurrence of the word under the cursor&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gf&lt;/code&gt; opens the file whose path is under the cursor&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Open a large file from a real project. Set &lt;code&gt;ma&lt;/code&gt; where you are, jump to another section with &lt;code&gt;/&lt;/code&gt;, come back with &lt;code&gt;`a&lt;/code&gt;. Make a small edit, jump with &lt;code&gt;gg&lt;/code&gt;, come back with &lt;code&gt;g;&lt;/code&gt;. If there's any file path referenced in the code, place your cursor on it and try &lt;code&gt;gf&lt;/code&gt;. Then run &lt;code&gt;:marks&lt;/code&gt; and &lt;code&gt;:jumps&lt;/code&gt; to see both lists — everything you just did is recorded there.&lt;/p&gt;

&lt;p&gt;Marks and jumps close out Vim's core navigation system. You now have the tools to move through any codebase without losing the thread. In the next tutorial, we'll cover &lt;strong&gt;text objects&lt;/strong&gt; — what many consider Vim's actual superpower: the ability to operate on "the content inside these quotes," "the block within these braces," or "this whole word" with a single two-key command.&lt;/p&gt;

&lt;p&gt;Never stop coding!&lt;/p&gt;

</description>
      <category>vim</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
