<?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>Git LFS: managing large files without breaking your repository</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Wed, 23 Sep 2026 15:50:35 +0000</pubDate>
      <link>https://dev.to/fj_palacios/git-lfs-managing-large-files-without-breaking-your-repository-19di</link>
      <guid>https://dev.to/fj_palacios/git-lfs-managing-large-files-without-breaking-your-repository-19di</guid>
      <description>&lt;p&gt;Someone on the team added assets to the repository. Figma exports, a design system PSD, the demo video linked from the README, a database snapshot "just for testing" that nobody deleted because nobody was sure if it was still needed. They added it the same way they'd add any file: &lt;code&gt;git add&lt;/code&gt;, &lt;code&gt;git commit&lt;/code&gt;, &lt;code&gt;git push&lt;/code&gt;. Git accepted everything without complaint. Git doesn't have opinions about this kind of thing.&lt;/p&gt;

&lt;p&gt;You find out it's a problem when you try to clone the repo on a new machine. Four minutes in, you're at 23%.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git count-objects &lt;span class="nt"&gt;-vH&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight properties"&gt;&lt;code&gt;&lt;span class="py"&gt;count&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3124&lt;/span&gt;
&lt;span class="py"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;412.50 KiB&lt;/span&gt;
&lt;span class="py"&gt;in-pack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;8932&lt;/span&gt;
&lt;span class="py"&gt;packs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;2&lt;/span&gt;
&lt;span class="py"&gt;size-pack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;4.73 GiB&lt;/span&gt;
&lt;span class="py"&gt;prune-packable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;garbage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0&lt;/span&gt;
&lt;span class="py"&gt;size-garbage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;0 bytes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;4.73 GB. For a web app. No videos, according to a quick scan of the current branch. Except eight months ago someone did add a 600 MB database snapshot and then deleted it in the next commit — from the working tree. In Git, deleting a file from the tree doesn't remove it from history. The object stays in the packfile, riding along with every clone, indefinitely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Git and binary files don't get along
&lt;/h2&gt;

&lt;p&gt;Git handles text files remarkably well. When you modify a source file, Git doesn't store the whole thing again — it stores a &lt;strong&gt;delta&lt;/strong&gt;: exactly what changed between the previous version and the new one. A 50 KB file with twenty modified lines produces a delta measured in bytes. History stays lean. Clones are fast.&lt;/p&gt;

&lt;p&gt;Binary files break that model entirely. A 200 MB PSD is, from Git's perspective, an opaque sequence of bytes with no recognizable structure. There's no useful delta to compute. So Git stores the full file every time it changes.&lt;/p&gt;

&lt;p&gt;200 MB × 50 design revisions = 10 GB that every developer downloads when they clone, forever, regardless of whether the project still exists, the designer moved on, or the file was "deleted" three versions ago.&lt;/p&gt;

&lt;p&gt;This isn't a Git bug. It's a mismatch: Git was designed for source code, and large binaries simply don't fit the model it was built around.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Git LFS does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Git LFS&lt;/strong&gt; (Large File Storage) is an official Git extension that solves the problem with a clean idea: instead of storing the binary in the repository, it stores a &lt;strong&gt;pointer&lt;/strong&gt; — a 130-byte text file that says "the real file is on this LFS server, it has this hash, and it weighs this much."&lt;/p&gt;

&lt;p&gt;Your Git repository only versions that pointer. The binary lives on a separate storage server — GitHub LFS, GitLab LFS, or one you run yourself. When you need the actual file, Git LFS downloads it automatically on checkout.&lt;/p&gt;

&lt;p&gt;A pointer file 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;version https://git-lfs.github.com/spec/v1
oid sha256:4d7a2f8c1e9b3d6a5c0e7f2a8b4d9c1e3f5a7b2d4c6e8f0a1b3c5d7e9f1a2b3c
size 209715200
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That three-line text file represents your 200 MB Photoshop project. It's a bit absurd. It also works.&lt;/p&gt;

&lt;p&gt;Your repository stays lightweight because Git history only contains pointers. The heavy files are downloaded on demand — only when you check out a commit that references them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;

&lt;p&gt;Git LFS is a separate binary you install alongside Git:&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-lfs

&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
apt &lt;span class="nb"&gt;install &lt;/span&gt;git-lfs

&lt;span class="c"&gt;# Arch Linux&lt;/span&gt;
pacman &lt;span class="nt"&gt;-S&lt;/span&gt; git-lfs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing, activate it once for your user account:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git lfs &lt;span class="nb"&gt;install&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;Updated Git hooks.
Git LFS initialized.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This sets up the Git hooks LFS needs to intercept push and pull operations. Skip this step and &lt;code&gt;git lfs track&lt;/code&gt; will appear to work but accomplish nothing — silently, with no error, which is the worst kind of not working.&lt;/p&gt;

&lt;h2&gt;
  
  
  Telling LFS what to track
&lt;/h2&gt;

&lt;p&gt;Inside your repository, specify which file types LFS should manage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git lfs track &lt;span class="s2"&gt;"*.psd"&lt;/span&gt;
git lfs track &lt;span class="s2"&gt;"*.mp4"&lt;/span&gt;
git lfs track &lt;span class="s2"&gt;"*.zip"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ The quotes are not optional. Without them, your shell expands the glob before LFS sees it, and LFS ends up registering only the specific files that exist in the current directory right now — not a general pattern. Every new asset you add later goes through regular Git, untracked by LFS, and you're back where you started.&lt;/p&gt;

&lt;p&gt;These commands write to &lt;code&gt;.gitattributes&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;*.&lt;span class="n"&gt;psd&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt;=&lt;span class="n"&gt;lfs&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt;=&lt;span class="n"&gt;lfs&lt;/span&gt; &lt;span class="n"&gt;merge&lt;/span&gt;=&lt;span class="n"&gt;lfs&lt;/span&gt; -&lt;span class="n"&gt;text&lt;/span&gt;
*.&lt;span class="n"&gt;mp4&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt;=&lt;span class="n"&gt;lfs&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt;=&lt;span class="n"&gt;lfs&lt;/span&gt; &lt;span class="n"&gt;merge&lt;/span&gt;=&lt;span class="n"&gt;lfs&lt;/span&gt; -&lt;span class="n"&gt;text&lt;/span&gt;
*.&lt;span class="n"&gt;zip&lt;/span&gt; &lt;span class="n"&gt;filter&lt;/span&gt;=&lt;span class="n"&gt;lfs&lt;/span&gt; &lt;span class="n"&gt;diff&lt;/span&gt;=&lt;span class="n"&gt;lfs&lt;/span&gt; &lt;span class="n"&gt;merge&lt;/span&gt;=&lt;span class="n"&gt;lfs&lt;/span&gt; -&lt;span class="n"&gt;text&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;.gitattributes&lt;/code&gt; is committed to the repository — unlike the hooks living in &lt;code&gt;.git/hooks/&lt;/code&gt; that we covered in the &lt;a href="https://dev.to/en/tutorials/git-hooks"&gt;previous Git hooks tutorial&lt;/a&gt;. When someone clones the repo, LFS reads that file to know exactly what it's responsible for. 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;git add .gitattributes
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: configure Git LFS tracking"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The daily workflow
&lt;/h2&gt;

&lt;p&gt;Here's the good part: once LFS is configured, nothing changes in how you work.&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;# Add a large file exactly as you normally would&lt;/span&gt;
git add assets/demo-video.mp4

&lt;span class="c"&gt;# Commit it&lt;/span&gt;
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: add product demo video"&lt;/span&gt;

&lt;span class="c"&gt;# On push, LFS sends the binary to the LFS server&lt;/span&gt;
&lt;span class="c"&gt;# and the pointer goes into your Git repository&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;LFS is entirely transparent. No special commands for your regular workflow. What actually happens on push is different — the binary goes to the LFS server, the pointer goes to Git — but from your side, it looks the same.&lt;/p&gt;

&lt;p&gt;To see which files are currently managed by LFS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git lfs ls-files
&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;8f3c2a1 * assets/demo-video.mp4
4d7a219 * design/logo-v3.psd
a1b2c3d * exports/database-snapshot.zip
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cloning repositories that use LFS
&lt;/h2&gt;

&lt;p&gt;When you clone a repo that uses LFS, tracked files download automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/your-org/your-repo.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want to clone without downloading the binaries — a CI pipeline that only needs the code, for example:&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="nv"&gt;GIT_LFS_SKIP_SMUDGE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 git clone https://github.com/your-org/your-repo.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pointers land in the working tree, but the actual files don't. Fetch specific ones when you need them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git lfs pull &lt;span class="nt"&gt;--include&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"assets/demo-video.mp4"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For CI pipelines running dozens of times per day, skipping LFS downloads where they're not needed isn't a minor optimization — it's the difference between a bandwidth quota that lasts the month and one that runs out before the second week.&lt;/p&gt;

&lt;h2&gt;
  
  
  The limits nobody mentions until you hit them
&lt;/h2&gt;

&lt;p&gt;This is where Git LFS stops feeling like a free solution and starts feeling like a product decision.&lt;/p&gt;

&lt;p&gt;GitHub Free includes &lt;strong&gt;10 GB of storage and 10 GB of bandwidth per month&lt;/strong&gt;. That bandwidth limit is not for what you upload — it's for what gets downloaded. Every clone, every CI checkout, every deploy that pulls your assets counts against your quota.&lt;/p&gt;

&lt;p&gt;10 GB sounds reasonable. Then you have eight developers cloning the repo regularly, a pipeline running forty times a day, and a designer pushing updated assets every other week. Turns out it goes faster than you'd expect.&lt;/p&gt;

&lt;p&gt;When you exceed the quota, GitHub disables LFS support until the next billing cycle. Not a warning, not a slowdown — disabled. Clones start failing in confusing ways because pointers can no longer be resolved, and anyone troubleshooting the problem without knowing LFS is involved is going to have a rough afternoon.&lt;/p&gt;

&lt;p&gt;Options when the free quota isn't enough:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub data pack&lt;/strong&gt;: 50 GB of storage + 50 GB of bandwidth for $5/month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Self-hosted LFS server&lt;/strong&gt;: more control, more setup and maintenance work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reconsider the architecture&lt;/strong&gt;: do those files actually need to be in Git?&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Alternatives depending on what you're storing
&lt;/h2&gt;

&lt;p&gt;Git LFS isn't the only option, and for some use cases it's not the right one.&lt;/p&gt;

&lt;h3&gt;
  
  
  For machine learning datasets: DVC
&lt;/h3&gt;

&lt;p&gt;If you're working with trained models, datasets, or ML experiments, &lt;a href="https://dvc.org/" rel="noopener noreferrer"&gt;DVC&lt;/a&gt; is built specifically for that use case. It works on top of Git, versions data alongside code, and supports S3, GCS, and Azure as storage backends. The integration with ML tooling is considerably better than anything you'd get from LFS.&lt;/p&gt;

&lt;h3&gt;
  
  
  For static assets that don't need versioning
&lt;/h3&gt;

&lt;p&gt;If the asset doesn't need a version history — a marketing banner, a demo video that gets fully replaced rather than revised, a static PDF — an S3 bucket or a CDN is cheaper and simpler than LFS. The repository references the URL; the file lives outside of Git entirely.&lt;/p&gt;

&lt;h3&gt;
  
  
  For the advanced case: Git Annex
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://git-annex.branchable.com/" rel="noopener noreferrer"&gt;Git Annex&lt;/a&gt; allows distributing large files with per-repository granularity — you can have clones that only download the files they actually need, not everything. The configuration curve is steeper than LFS; for most teams, LFS is sufficient.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Git stores binary files as full objects because it can't compute useful deltas on them — history grows linearly with every revision.&lt;/li&gt;
&lt;li&gt;Git LFS stores a 130-byte pointer in your repository; the actual file goes to a separate server.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git lfs install&lt;/code&gt; runs once per user account, not once per repository.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git lfs track "*.psd"&lt;/code&gt; always needs quotes so it registers a pattern, not just current files.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.gitattributes&lt;/code&gt; is committed to the repository — unlike hooks in &lt;code&gt;.git/hooks/&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The normal workflow (add, commit, push) doesn't change after setup.&lt;/li&gt;
&lt;li&gt;GitHub Free includes 10 GB of storage and 10 GB of bandwidth per month; exceeding either disables LFS.&lt;/li&gt;
&lt;li&gt;In high-frequency CI/CD pipelines, use &lt;code&gt;GIT_LFS_SKIP_SMUDGE=1&lt;/code&gt; where you don't need the binaries.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Git LFS isn't a perfect solution — the bandwidth limits surface faster than expected, and it doesn't help with large files already buried in your history (that requires rewriting the past, which is its own category of adventure). But it's the right answer to the most common version of the problem: someone committed large binary files, the repository quietly ballooned, and now every clone is a patience exercise.&lt;/p&gt;

&lt;p&gt;That wraps up &lt;strong&gt;Module 8 — Advanced Git Features&lt;/strong&gt;. Next, we start Module 9 — Productivity and Tooling, where we'll look at how to make Git work faster for you with configuration, aliases, and tools built around real-world workflows.&lt;/p&gt;

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

</description>
      <category>git</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Git Worktrees: multiple working directories from a single repository</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Tue, 22 Sep 2026 14:26:20 +0000</pubDate>
      <link>https://dev.to/fj_palacios/git-worktrees-multiple-working-directories-from-a-single-repository-2j5c</link>
      <guid>https://dev.to/fj_palacios/git-worktrees-multiple-working-directories-from-a-single-repository-2j5c</guid>
      <description>&lt;p&gt;Somewhere on your machine there's a folder called &lt;code&gt;my-project-2&lt;/code&gt;. Or &lt;code&gt;my-project-backup&lt;/code&gt;. Or, if you've been at this long enough, &lt;code&gt;my-project-FINAL-no-really-this-one&lt;/code&gt;. You don't bring it up in standups. It doesn't live inside the repo, so it's not in &lt;code&gt;.gitignore&lt;/code&gt; either. It just exists, quietly, at some undisclosed path in your filesystem, doing the job your first manual clone was supposed to handle.&lt;/p&gt;

&lt;p&gt;Or you do the &lt;a href="https://dev.to/en/tutorials/stashing-changes-git-stash"&gt;stash&lt;/a&gt; dance: &lt;code&gt;git stash&lt;/code&gt;, switch branch, do the thing, &lt;code&gt;git stash pop&lt;/code&gt;, untangle whatever the pop left behind — and there's always something — then get back to where you were, slightly less sure of exactly where that was. Every time. It works. Like leaving one screw out of an IKEA shelf works: the thing is standing, but you're not entirely confident it should be.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git worktree&lt;/code&gt; isn't some obscure command for people who read Git documentation for fun. It's the official answer Git had prepared while everyone was making manual clones and calling it a workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a worktree is and why it exists
&lt;/h2&gt;

&lt;p&gt;A clone? A symlink? A renamed copy of the repository? None of those.&lt;/p&gt;

&lt;p&gt;A &lt;strong&gt;worktree&lt;/strong&gt; is a working directory linked to an existing Git repository. Each worktree has its own active branch, its own working tree and staging area — but they share the same history, the same objects, the same refs. Technical version: multiple working environments connected to the same underlying &lt;code&gt;.git&lt;/code&gt;. Tuesday morning version: you can have &lt;code&gt;my-project-hotfix/&lt;/code&gt; and &lt;code&gt;my-project-feature/&lt;/code&gt; open in your editor at the same time, each on its own branch, and when you commit in one, the other knows.&lt;/p&gt;

&lt;p&gt;Think of it as multiple workbenches. One has the feature in progress. One has the urgent hotfix. One has the teammate's branch you need to review. None of them interfere with the others. When you're done with one, you close it and it's gone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-project/             ← main directory, branch feature/login
├── .git/               ← the real .git folder lives here
└── src/

my-project-hotfix/      ← worktree, branch hotfix/critical-bug
├── .git                ← file, not folder
└── src/

my-project-review/      ← worktree, branch fix/auth-issue
├── .git                ← file, not folder
└── src/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Secondary worktrees have a &lt;code&gt;.git&lt;/code&gt; file — not a folder, a plain text file pointing to the main repository. Git handles all the internal plumbing. You don't need to open it, understand it, or think about it; just know that it's what separates a worktree from a clone. All three directories share the disk space for Git history. The only rule: two worktrees can't be on the same branch at the same time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating your first worktree
&lt;/h2&gt;

&lt;p&gt;The basic command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add ../my-project-review fix/auth-issue
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates &lt;code&gt;../my-project-review&lt;/code&gt; and checks out &lt;code&gt;fix/auth-issue&lt;/code&gt; there. If the branch doesn't exist yet, create it in the same step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add &lt;span class="nt"&gt;-b&lt;/span&gt; feature/new-feature ../my-project-feature main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;-b feature/new-feature&lt;/code&gt; creates the branch from &lt;code&gt;main&lt;/code&gt;. The directory exists, the branch exists, you can work — without touching anything in your current directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Listing all active worktrees
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree list
&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/fjpalacios/projects/my-project          9f3c2a1 [feature/login]
/home/fjpalacios/projects/my-project-hotfix   3b7d9e2 [hotfix/critical-bug]
/home/fjpalacios/projects/my-project-review   8c1f4d3 [fix/auth-issue]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Path, current commit, active branch. Git keeps a record of all worktrees in the repository and shows them here. If you try to check out a branch that's already active in another worktree, Git stops you — not because it's forbidden, but because having the same branch in two places is just confusing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Removing a worktree
&lt;/h2&gt;

&lt;p&gt;When you're done:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree remove ../my-project-review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This removes the directory and cleans up the internal reference. If there are uncommitted changes, Git refuses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;error: '../my-project-review' contains modified or untracked files, use --force to override
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To force it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree remove &lt;span class="nt"&gt;--force&lt;/span&gt; ../my-project-review
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--force&lt;/code&gt; tells Git: "I know what I'm doing, delete without asking." Git always believes you — that's the issue. Use it when you're certain nothing will be lost. Not pretty sure. Not I think so. Certain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real use cases
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Reviewing a PR without leaving your work
&lt;/h3&gt;

&lt;p&gt;You're on &lt;code&gt;feature/dashboard&lt;/code&gt;, hours in with no commits. A PR comes in for review. Without worktrees: stash, switch, review, come back, pop, untangle. With worktrees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add ../review-pr-234 origin/feature/pr-to-review
&lt;span class="nb"&gt;cd&lt;/span&gt; ../review-pr-234
&lt;span class="c"&gt;# Review the code, run the tests, leave comments&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../my-project
&lt;span class="c"&gt;# Everything is exactly where you left it&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Urgent production hotfix
&lt;/h3&gt;

&lt;p&gt;You're on &lt;code&gt;feature/complex-refactor&lt;/code&gt;, two hours in without committing. Production reports a critical bug:&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;# Create the worktree directly from main&lt;/span&gt;
git worktree add &lt;span class="nt"&gt;-b&lt;/span&gt; hotfix/critical-login ../hotfix-urgent main

&lt;span class="c"&gt;# Fix, test, commit, and push from there&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../hotfix-urgent
&lt;span class="c"&gt;# ...fix, commit, push...&lt;/span&gt;
git push origin hotfix/critical-login

&lt;span class="c"&gt;# Your feature branch hasn't moved&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ../my-project
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No stash, no branch switch, no lost context. The main directory stays on &lt;code&gt;feature/complex-refactor&lt;/code&gt; exactly where you left it.&lt;/p&gt;

&lt;h3&gt;
  
  
  Comparing branches side by side
&lt;/h3&gt;

&lt;p&gt;You want to see the differences between &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;feature/new-branch&lt;/code&gt; directly in your editor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree add ../my-project-main main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open both directories in parallel — two terminal panes, or if you're on &lt;a href="https://dev.to/en/courses/mastering-vim-from-scratch"&gt;Neovim&lt;/a&gt;, diff them directly with &lt;code&gt;vimdiff&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;vimdiff ../my-project/src/auth.ts ../my-project-main/src/auth.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For those who prefer a mouse and don't mind the Electron startup tax, &lt;code&gt;code --diff&lt;/code&gt; does the same.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleaning up orphaned worktrees
&lt;/h2&gt;

&lt;p&gt;Sometimes a worktree ends up in ghost state: you deleted the directory by hand, or removed the branch before the worktree. Git doesn't clean up what it doesn't understand — it keeps the reference registered indefinitely, and throws this error the next time you try to create something with the same name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fatal: '/home/fjpalacios/projects/my-project-old' already exists
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see all worktrees in detail, including the problematic ones:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree list &lt;span class="nt"&gt;--porcelain&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To clean up references to worktrees no longer on disk:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git worktree prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  When not to use worktrees
&lt;/h2&gt;

&lt;p&gt;Worktrees don't replace stash, they don't replace branches, and they're not the tool for every context switch:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If you switch branches constantly in the same directory&lt;/strong&gt;: worktrees work best when each one has a clear mission and a relatively long life. For fast, frequent context switches, stash is still more direct.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If the repository has large files&lt;/strong&gt;: each worktree needs its own working tree. History is shared, but directory contents aren't — if the repo is heavy, multiply accordingly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If you only need to look at another branch without working on it&lt;/strong&gt;: &lt;code&gt;git show branch:path/to/file&lt;/code&gt; or &lt;code&gt;git diff main..feature&lt;/code&gt; is enough. You don't need a full directory for that.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Worktrees make explicit something that was already happening — chaotically — through manual clones and the stash dance: sometimes you need to be in more than one place at once. &lt;code&gt;my-project-2&lt;/code&gt; can finally retire. Git had the infrastructure for this all along; it was just waiting for you to use it.&lt;/p&gt;

&lt;p&gt;Next tutorial: &lt;strong&gt;Git LFS&lt;/strong&gt; — how to manage large files (binaries, assets, datasets) that shouldn't live in your normal Git history.&lt;/p&gt;

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

</description>
      <category>git</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Docker image optimization: from 1.2 GB to 85 MB</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Mon, 21 Sep 2026 14:47:50 +0000</pubDate>
      <link>https://dev.to/fj_palacios/docker-image-optimization-from-12-gb-to-85-mb-52f4</link>
      <guid>https://dev.to/fj_palacios/docker-image-optimization-from-12-gb-to-85-mb-52f4</guid>
      <description>&lt;p&gt;At some point you run &lt;code&gt;docker images&lt;/code&gt; and see a number that doesn't look right. A Node.js API with three endpoints and a database connection: 1.2 GB. A Go binary that compiled to 8 MB, sitting inside an 823 MB image. A Python script that reads a CSV and returns JSON: 980 MB.&lt;/p&gt;

&lt;p&gt;The container works. It deploys fine. And it's carrying a compiler, a package manager, three versions of libssl, and an entire Debian system that hasn't executed a single instruction since it booted.&lt;/p&gt;

&lt;p&gt;This is the last lesson in the images module, and it's where we close the loop: take everything from &lt;a href="https://dev.to/en/tutorials/docker-multi-stage-builds"&gt;multi-stage builds&lt;/a&gt;, &lt;a href="https://dev.to/en/tutorials/docker-buildkit"&gt;BuildKit&lt;/a&gt;, and &lt;a href="https://dev.to/en/tutorials/dockerfile-best-practices-security"&gt;security practices&lt;/a&gt; and produce images that contain exactly what they need and nothing more.&lt;/p&gt;

&lt;h2&gt;
  
  
  The base image pyramid
&lt;/h2&gt;

&lt;p&gt;The choice of base image is the single highest-leverage decision in image optimization. The size differences aren't marginal:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Base&lt;/th&gt;
&lt;th&gt;Approximate size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node:20&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~1.1 GB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ubuntu:22.04&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~77 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;debian:12-slim&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~74 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;node:20-alpine&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~135 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;alpine:3.19&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;~7 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;scratch&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;0 MB&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;node:20&lt;/code&gt; is built on &lt;code&gt;debian:12&lt;/code&gt;. &lt;code&gt;node:20-alpine&lt;/code&gt; is built on &lt;code&gt;alpine:3.19&lt;/code&gt;. The difference isn't which version of Node they ship — both ship Node 20. The difference is how much operating system comes with it.&lt;/p&gt;

&lt;p&gt;Alpine uses musl libc instead of glibc, busybox instead of bash and coreutils, and apk instead of apt. For most applications this makes no difference. For some it does — native dependencies that expect glibc will fail in ways that aren't immediately obvious. Worth testing before assuming compatibility.&lt;/p&gt;

&lt;p&gt;Switching from &lt;code&gt;node:20&lt;/code&gt; to &lt;code&gt;node:20-alpine&lt;/code&gt; is the highest-ROI optimization in this entire lesson. One line change. 88% smaller image.&lt;/p&gt;

&lt;h3&gt;
  
  
  scratch: the extreme case
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;FROM scratch&lt;/code&gt; is exactly what it sounds like: an empty filesystem. No OS, no shell, nothing. It only works for fully static binaries, which in Go is the default with &lt;code&gt;CGO_ENABLED=0&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1.22-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 &lt;span class="nv"&gt;GOOS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;linux go build &lt;span class="nt"&gt;-ldflags&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"-w -s"&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; app .

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; scratch&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/app /app&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first time you see the result — 8 MB where there used to be 800 — it feels like finding a cheat code. The second time you try it on a different application, it doesn't start because there are no SSL certificates for outbound HTTPS, and you realize scratch means it. No certificates, no timezone data, nothing you didn't explicitly copy.&lt;/p&gt;

&lt;p&gt;Both reactions are completely expected. For runtimes like Node.js, Python, or Java, scratch isn't the answer. That's where distroless comes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distroless images
&lt;/h2&gt;

&lt;p&gt;Google maintains a set of images designed for exactly this scenario: they contain only what's needed to run your application — the runtime, SSL certificates, timezone data — without a shell, without a package manager, without anything you'd use to poke around inside a container.&lt;/p&gt;

&lt;p&gt;If someone asks "what's the most secure minimal base for a production container?", distroless is the answer without needing to think about it. No shell means no shell exploitation. No package manager means no package manager exploitation. Minimal attack surface as a design principle, not as an afterthought.&lt;/p&gt;

&lt;p&gt;For Go (or any static binary):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;golang:1.22-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nv"&gt;CGO_ENABLED&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0 go build &lt;span class="nt"&gt;-o&lt;/span&gt; app .

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gcr.io/distroless/static-debian12&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/app /app&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["/app"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;static-debian12&lt;/code&gt; ships SSL certificates and timezone data. It weighs around 2 MB. The benefits of scratch without having to remember what you forgot to include.&lt;/p&gt;

&lt;p&gt;For Node.js:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm prune &lt;span class="nt"&gt;--production&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; gcr.io/distroless/nodejs22-debian12&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["dist/index.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Available distroless images for the most common runtimes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Runtime&lt;/th&gt;
&lt;th&gt;Image&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Static binary&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcr.io/distroless/static-debian12&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;glibc&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcr.io/distroless/base-debian12&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Node.js 22&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcr.io/distroless/nodejs22-debian12&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python 3&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcr.io/distroless/python3-debian12&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Java 21&lt;/td&gt;
&lt;td&gt;&lt;code&gt;gcr.io/distroless/java21-debian12&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;One honest limitation: no shell means no &lt;code&gt;docker exec -it &amp;lt;container&amp;gt; sh&lt;/code&gt;. If you're used to jumping inside containers to inspect things, distroless changes that workflow. The &lt;code&gt;:debug&lt;/code&gt; variant adds busybox and enables exec — use it for development environments where you need to debug, not for production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Analyzing your image with dive
&lt;/h2&gt;

&lt;p&gt;Before optimizing anything, you need to know what's taking up space. &lt;code&gt;docker history&lt;/code&gt; gives you layer sizes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;history &lt;/span&gt;my-app:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;IMAGE         CREATED BY                                      SIZE
a1b2c3d4e5f6  CMD ["node", "dist/index.js"]                  0B
...           COPY --from=builder /app/dist ./dist            2.3MB
...           COPY --from=builder /app/node_modules ./node_m  198MB
...           RUN npm ci --only=production                    0B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful, but it doesn't tell you what's inside each layer. For that, there's &lt;code&gt;dive&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Installation
&lt;/h3&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;dive

&lt;span class="c"&gt;# Ubuntu / Debian — dive isn't in the official repos; easiest path is running it via Docker:&lt;/span&gt;
docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="nt"&gt;-it&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock wagoodman/dive:latest &amp;lt;image&amp;gt;

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

&lt;/div&gt;



&lt;h3&gt;
  
  
  Basic usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;dive my-app:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Opens an interactive terminal UI where you can navigate through each layer and see exactly which files were added, modified, or deleted.&lt;/p&gt;

&lt;p&gt;There's a specific moment everyone has in their first dive session: you find the layer taking up 60% of the image size, expand it, and recognize it. It's your dev dependencies. Not the base image's — yours. The ones that ended up there because &lt;code&gt;COPY --from=builder /app/node_modules&lt;/code&gt; copied everything without filtering, and the build stage installed everything including typescript, jest, all the &lt;code&gt;@types/*&lt;/code&gt; packages, and whatever else your development workflow needs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total Image size: 487 MB
Potential wasted space: 0 B
Image efficiency score: 87%

Layer 3: COPY --from=builder /app/node_modules ./node_modules  [312 MB]
  └── node_modules/
       ├── typescript/          [28 MB]  ← not needed at runtime
       ├── @types/              [15 MB]  ← definitely not
       ├── jest/                [12 MB]  ← also no
       └── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# CI mode: returns pass/fail based on image efficiency&lt;/span&gt;
&lt;span class="nv"&gt;CI&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;dive my-app:latest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In CI pipelines, this flag evaluates whether files are deleted in later layers (a sign that cleanup could have happened earlier) and returns a non-zero exit code if the image doesn't pass the efficiency threshold. Useful for preventing poorly optimized images from reaching production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer optimization
&lt;/h2&gt;

&lt;p&gt;Choosing the right base gets you most of the way there. The rest comes from how you write &lt;code&gt;RUN&lt;/code&gt; instructions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Clean up in the same layer
&lt;/h3&gt;

&lt;p&gt;Each &lt;code&gt;RUN&lt;/code&gt; instruction creates a new layer. If you add files in one &lt;code&gt;RUN&lt;/code&gt; and delete them in the next, &lt;strong&gt;the files are still in the image&lt;/strong&gt; — they're marked as deleted in the new layer, but the original layer with the files is still there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ The apt files are still in the image — the previous layer keeps them&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;

&lt;span class="c"&gt;# ✅ Cleanup happens in the same layer, no trace left&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; /var/lib/apt/lists/&lt;span class="k"&gt;*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same principle applies to anything that downloads temporary files during installation: do it all in one instruction.&lt;/p&gt;

&lt;h3&gt;
  
  
  npm prune --production
&lt;/h3&gt;

&lt;p&gt;If your Dockerfile installs all dependencies (including dev) to compile the application and then copies &lt;code&gt;node_modules&lt;/code&gt; to the final stage unfiltered, you're shipping bundlers, linters, TypeScript types, and testing frameworks to production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci                     &lt;span class="c"&gt;# Installs everything: dev + production&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build
&lt;span class="k"&gt;RUN &lt;/span&gt;npm prune &lt;span class="nt"&gt;--production&lt;/span&gt;     &lt;span class="c"&gt;# Removes dev dependencies before copying&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/index.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  .dockerignore
&lt;/h3&gt;

&lt;p&gt;The easiest thing to forget and one of the highest-impact optimizations. Without a &lt;code&gt;.dockerignore&lt;/code&gt;, you're sending &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;.git&lt;/code&gt;, logs, and everything in your working directory to the Docker daemon before the build even starts. That slows down the build context transfer and can end up in the image if you have any imprecise &lt;code&gt;COPY .&lt;/code&gt; instructions.&lt;/p&gt;

&lt;p&gt;A minimal &lt;code&gt;.dockerignore&lt;/code&gt; for Node.js projects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;node_modules&lt;/span&gt;
.&lt;span class="n"&gt;git&lt;/span&gt;
.&lt;span class="n"&gt;env&lt;/span&gt;
*.&lt;span class="n"&gt;log&lt;/span&gt;
&lt;span class="n"&gt;dist&lt;/span&gt;
&lt;span class="n"&gt;coverage&lt;/span&gt;
.&lt;span class="n"&gt;DS_Store&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Before and after: the complete Dockerfile
&lt;/h2&gt;

&lt;p&gt;Putting it all together — multi-stage builds, BuildKit cache mounts, non-root user, and the optimization techniques from this lesson:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ❌ BEFORE: 1.2 GB&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "index.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ✅ AFTER: ~85 MB (93% reduction)&lt;/span&gt;
&lt;span class="c"&gt;# syntax=docker/dockerfile:1&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/root/.npm &lt;span class="se"&gt;\
&lt;/span&gt;    npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; src/ ./src/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; tsconfig.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm prune &lt;span class="nt"&gt;--production&lt;/span&gt;

&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;addgroup &lt;span class="nt"&gt;-g&lt;/span&gt; 1001 &lt;span class="nt"&gt;-S&lt;/span&gt; appgroup &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    adduser &lt;span class="nt"&gt;-u&lt;/span&gt; 1001 &lt;span class="nt"&gt;-S&lt;/span&gt; appuser &lt;span class="nt"&gt;-G&lt;/span&gt; appgroup
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder --chown=appuser:appgroup /app/node_modules ./node_modules&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; --from=builder --chown=appuser:appgroup /app/dist ./dist&lt;/span&gt;
&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="s"&gt; appuser&lt;/span&gt;
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "dist/index.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The differences: Alpine instead of full Debian, multi-stage to isolate the build environment, prune to keep only runtime dependencies, non-root user. None of these changes take more than a few minutes — and the result is an image that transfers ten times faster and has a fraction of the attack surface.&lt;/p&gt;




&lt;p&gt;That closes the images module. We've gone from knowing what a Dockerfile is to building images that are efficient, secure, and production-ready. Next up is Module 3 — Docker Compose: how to orchestrate multiple containers that need to work together — database, backend, frontend — with a single configuration file and one command.&lt;/p&gt;

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




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Run &lt;code&gt;dive&lt;/code&gt; against any image you have over 500 MB. Find the three heaviest layers and identify which technique from this lesson would reduce the size the most. If you can get it under 100 MB, you've got it.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>BuildKit in Docker: build secrets, dependency caching, and multi-platform images</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Thu, 17 Sep 2026 10:30:39 +0000</pubDate>
      <link>https://dev.to/fj_palacios/buildkit-in-docker-build-secrets-dependency-caching-and-multi-platform-images-4ad1</link>
      <guid>https://dev.to/fj_palacios/buildkit-in-docker-build-secrets-dependency-caching-and-multi-platform-images-4ad1</guid>
      <description>&lt;p&gt;If you've been using Docker for a while, there's a good chance your builds spend a couple of minutes downloading the exact same packages every single time. npm install, pip install, bundle install. Everything. From scratch. The lockfile hasn't changed. The packages haven't changed. Docker just doesn't know that, and nobody told you it could.&lt;/p&gt;

&lt;p&gt;Not a Docker design philosophy about build purity. Just something nobody told you existed.&lt;/p&gt;

&lt;p&gt;There's also something we left hanging in the &lt;a href="https://dev.to/en/tutorials/advanced-dockerfile-instructions"&gt;previous lesson&lt;/a&gt;: &lt;code&gt;ARG&lt;/code&gt; leaves values in the image history, visible to anyone who runs &lt;code&gt;docker history&lt;/code&gt;. BuildKit has the real solution for that too.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;BuildKit&lt;/strong&gt; is Docker's modern build engine. It's been available since Docker 18.09 and has been the default backend since version 23.0. If you have Docker installed in 2024 or 2025, it's already active. What you probably haven't set up are the features that make it actually useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking BuildKit and the syntax pragma
&lt;/h2&gt;

&lt;p&gt;On Docker Desktop (Mac and Windows), &lt;code&gt;docker buildx&lt;/code&gt; comes included. On Linux installed from system packages — Arch, Ubuntu, Debian — the plugin is optional and needs to be installed separately:&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;docker-buildx

&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
apt &lt;span class="nb"&gt;install &lt;/span&gt;docker-buildx-plugin

&lt;span class="c"&gt;# Arch Linux&lt;/span&gt;
pacman &lt;span class="nt"&gt;-S&lt;/span&gt; docker-buildx
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Verify it's available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx 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;github.com/docker/buildx v0.34.1 ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're on Docker 23.0 or later — which is any installation from 2023 onwards — BuildKit is already the default backend. Nothing else to enable. If you're on an older version for some reason, you can force it per-session with:&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="nv"&gt;DOCKER_BUILDKIT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 docker build &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Regardless of version, add this comment as the first line of your Dockerfiles:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# syntax=docker/dockerfile:1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, it's a comment that does something. BuildKit uses "frontends" — versioned Dockerfile parsers with their own independent release cycle. This pragma pins the frontend to the latest stable channel and is what gives you access to modern syntax like &lt;code&gt;--mount&lt;/code&gt; in &lt;code&gt;RUN&lt;/code&gt; instructions. Without it, Docker uses the version bundled with your installation, which may or may not include everything we're about to use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build secrets
&lt;/h2&gt;

&lt;p&gt;Quick recap from the previous lesson: passing secrets through &lt;code&gt;ARG&lt;/code&gt; leaves them embedded in the image history, readable by anyone with access to the image. BuildKit build secrets fix this cleanly: the build can read the secret while it needs it, but it never ends up in any layer.&lt;/p&gt;

&lt;p&gt;The mechanism has two parts. In the Dockerfile, you mount the secret inside the &lt;code&gt;RUN&lt;/code&gt; where you use it — and only there:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# syntax=docker/dockerfile:1&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;node:20-alpine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;builder&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret,id&lt;span class="o"&gt;=&lt;/span&gt;npm_token &lt;span class="se"&gt;\
&lt;/span&gt;    npm config &lt;span class="nb"&gt;set&lt;/span&gt; //registry.npmjs.org/:_authToken&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; /run/secrets/npm_token&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    npm ci &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    npm config delete //registry.npmjs.org/:_authToken
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the build command, you tell Docker where the secret comes from:&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;# From a file&lt;/span&gt;
docker build &lt;span class="nt"&gt;--secret&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;npm_token,src&lt;span class="o"&gt;=&lt;/span&gt;.npmtoken &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# From an environment variable&lt;/span&gt;
docker build &lt;span class="nt"&gt;--secret&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;npm_token,env&lt;span class="o"&gt;=&lt;/span&gt;NPM_TOKEN &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The secret is mounted at &lt;code&gt;/run/secrets/&amp;lt;id&amp;gt;&lt;/code&gt; for the duration of that &lt;code&gt;RUN&lt;/code&gt;. When the command finishes, it's gone. Not in the layer, not in the history. You can verify:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;history &lt;/span&gt;my-image
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;IMAGE         CREATED      CREATED BY
a1b2c3d4e5f6  2 hours ago  CMD ["node", "dist/index.js"]
...           ...          RUN --mount=type=secret,id=npm_token npm config set ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The command shows up. The value doesn't. Exactly what you've been trying to achieve since the &lt;a href="https://dev.to/en/tutorials/dockerfile-best-practices-security"&gt;Dockerfile security best practices lesson&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  SSH agent forwarding
&lt;/h3&gt;

&lt;p&gt;The same mechanism exists for SSH access — useful when a build needs to clone a private repository without keys ever touching the container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# syntax=docker/dockerfile:1&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;apk add &lt;span class="nt"&gt;--no-cache&lt;/span&gt; git openssh-client
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ssh &lt;span class="se"&gt;\
&lt;/span&gt;    git clone git@github.com:your-org/private-library.git /deps
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker build &lt;span class="nt"&gt;--ssh&lt;/span&gt; default &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;--ssh default&lt;/code&gt; forwards the host's SSH agent. Keys never touch the container filesystem, never end up in any layer, and once the &lt;code&gt;RUN&lt;/code&gt; finishes the access is gone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cache mounts
&lt;/h2&gt;

&lt;p&gt;The problem from the opening of this lesson has a solution that's exactly one flag long:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# syntax=docker/dockerfile:1&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; package*.json ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/root/.npm &lt;span class="se"&gt;\
&lt;/span&gt;    npm ci
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm run build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the whole thing. First run: npm downloads everything from the internet and stores the cache in &lt;code&gt;/root/.npm&lt;/code&gt;. Second run — even if you built from scratch, even if you deleted the previous image — the cache is still there. npm finds it, downloads nothing.&lt;/p&gt;

&lt;p&gt;The time difference is immediate on projects with any meaningful dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;First run:   2m 21s  (full download from npm)
Second run:  8s      (cache hit, zero downloads)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The cache path changes depending on the package manager, but the pattern is always the same:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# npm&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/root/.npm &lt;span class="se"&gt;\
&lt;/span&gt;    npm ci

&lt;span class="c"&gt;# pip&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/root/.cache/pip &lt;span class="se"&gt;\
&lt;/span&gt;    pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="c"&gt;# Cargo (Rust)&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/usr/local/cargo/registry &lt;span class="se"&gt;\
&lt;/span&gt;    cargo build &lt;span class="nt"&gt;--release&lt;/span&gt;

&lt;span class="c"&gt;# apt (Debian/Ubuntu)&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/var/cache/apt,sharing&lt;span class="o"&gt;=&lt;/span&gt;locked &lt;span class="se"&gt;\
&lt;/span&gt;    &lt;span class="nt"&gt;--mount&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache,target&lt;span class="o"&gt;=&lt;/span&gt;/var/lib/apt,sharing&lt;span class="o"&gt;=&lt;/span&gt;locked &lt;span class="se"&gt;\
&lt;/span&gt;    apt update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; curl
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;sharing=locked&lt;/code&gt; parameter in the apt example prevents two parallel builds from writing to the cache simultaneously — without it, the apt database can get corrupted if you run concurrent builds.&lt;/p&gt;

&lt;p&gt;One honest limitation: the cache lives on the build machine, not in the image. If you add a cache mount, it works great locally, and then you notice CI builds take exactly the same time as before — that's completely normal and doesn't mean you did anything wrong. CI runners are typically ephemeral: each job starts on a clean machine with no prior cache. The fix exists, but it's platform-specific. GitHub Actions, GitLab CI, and most modern providers support persisting BuildKit state between runs. Search "BuildKit cache" followed by your CI platform name.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multi-platform builds
&lt;/h2&gt;

&lt;p&gt;Building on a Mac with Apple Silicon? Deploying to x86_64? Any Raspberry Pi in the mix? If more than one architecture is in the picture, you've already seen an image that works perfectly on your machine and fails on the server. Or you've been maintaining two separate tags by building it in two different places.&lt;/p&gt;

&lt;p&gt;Multi-platform builds in BuildKit solve this: build the image for multiple architectures from a single machine and publish it under the same tag. Docker automatically selects the right one on &lt;code&gt;docker pull&lt;/code&gt; based on the host's architecture.&lt;/p&gt;

&lt;p&gt;First, create a builder with multi-platform support:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx create &lt;span class="nt"&gt;--use&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; multi-arch-builder
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then build and push. Multi-platform builds need &lt;code&gt;--push&lt;/code&gt; or &lt;code&gt;--output&lt;/code&gt; — they can't live in local cache only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx build &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--platform&lt;/span&gt; linux/amd64,linux/arm64 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-t&lt;/span&gt; your-registry/my-app:latest &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--push&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;BuildKit builds images for each platform in parallel. For architectures other than the host's native one, it uses QEMU emulation — which works, but turns your CPU into an actor playing a different CPU, with everything that implies for performance. A 45-second Go build can become an 8-minute one when QEMU translates every instruction on the fly. For compiled languages where build time matters, consider native runners for the target architecture.&lt;/p&gt;

&lt;p&gt;Node.js, Python, and other interpreted languages fare much better: the Dockerfile installs a runtime that already exists as a multi-arch image, and the source code doesn't require compilation.&lt;/p&gt;

&lt;p&gt;Check what platforms your builder supports:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker buildx inspect &lt;span class="nt"&gt;--bootstrap&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Name:      multi-arch-builder
Driver:    docker-container
Platforms: linux/amd64, linux/arm64, linux/arm/v7, linux/386, ...
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;BuildKit is the difference between builds that redo the same work every time and builds that protect secrets, reuse prior work, and run on any architecture. Not an advanced feature for edge cases — just the correct way to build images in 2026.&lt;/p&gt;

&lt;p&gt;In the next lesson, we close the images module with the final optimization techniques: distroless images, size analysis with &lt;code&gt;dive&lt;/code&gt;, and how to produce production images that contain exactly what they need and nothing else.&lt;/p&gt;

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




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Add a cache mount to the Dockerfile of any project you have. Run &lt;code&gt;docker build&lt;/code&gt; twice and compare the install step timing. If the second run isn't noticeably faster, check that the cache path matches the correct location for your package manager.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Git hooks: automating what nobody remembers to do</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Wed, 16 Sep 2026 08:15:41 +0000</pubDate>
      <link>https://dev.to/fj_palacios/git-hooks-automating-what-nobody-remembers-to-do-11dh</link>
      <guid>https://dev.to/fj_palacios/git-hooks-automating-what-nobody-remembers-to-do-11dh</guid>
      <description>&lt;p&gt;Your project's commit history reads: &lt;code&gt;fix&lt;/code&gt;. &lt;code&gt;stuff&lt;/code&gt;. &lt;code&gt;wip&lt;/code&gt;. &lt;code&gt;more stuff&lt;/code&gt;. &lt;code&gt;ok now fixed for real&lt;/code&gt;. At some point someone left a comment in a PR: &lt;em&gt;"hey, should we write more descriptive commit messages?"&lt;/em&gt; Twelve thumbs up. Zero commits changed.&lt;/p&gt;

&lt;p&gt;Because good intentions don't scale. Everyone agrees it's a good idea. Everyone has other things to do. And Git, which has the power to stop any of this, just executes what you tell it — no friction, no judgment, no opinion on whether "wip" adequately describes the two hundred lines you staged.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Git hooks&lt;/strong&gt; are how you add that opinion to the process. Not as a Confluence doc nobody reads after onboarding, not as a recurring conversation at standup — as scripts that run automatically at specific points in the Git workflow and can stop an operation if something doesn't look right.&lt;/p&gt;

&lt;h2&gt;
  
  
  What are Git hooks
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;hook&lt;/strong&gt; is a script Git runs before or after specific operations: creating a commit, validating a commit message, sending a push to the remote, receiving changes on the server.&lt;/p&gt;

&lt;p&gt;The logic is binary: if the script exits with code &lt;code&gt;0&lt;/code&gt;, Git proceeds. If it exits with anything else, the operation is aborted.&lt;/p&gt;

&lt;p&gt;A hook can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Run a linter against staged files before they become a commit.&lt;/li&gt;
&lt;li&gt;Check that the commit message follows &lt;a href="https://dev.to/en/tutorials/git-commit-best-practices"&gt;Conventional Commits format&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;Run the test suite before a push reaches the remote.&lt;/li&gt;
&lt;li&gt;Block direct pushes to &lt;code&gt;main&lt;/code&gt; on the server.&lt;/li&gt;
&lt;li&gt;Trigger a deploy after changes land on a release branch.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Git ships with over twenty defined hook points. In practice, most teams use three or four. Don't lose sleep over the full list.&lt;/p&gt;

&lt;h2&gt;
  
  
  How hooks work
&lt;/h2&gt;

&lt;p&gt;Hooks live in &lt;code&gt;.git/hooks/&lt;/code&gt;. Open that directory in any freshly initialized repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;ls&lt;/span&gt; .git/hooks/
&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;applypatch-msg.sample   post-update.sample      pre-commit.sample
commit-msg.sample       pre-applypatch.sample   pre-push.sample
post-checkout.sample    pre-merge-commit.sample pre-rebase.sample
post-commit.sample      pre-receive.sample      prepare-commit-msg.sample
update.sample
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything ends in &lt;code&gt;.sample&lt;/code&gt; — Git includes them as reference but doesn't run them. To activate a hook:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Remove the &lt;code&gt;.sample&lt;/code&gt; extension.&lt;/li&gt;
&lt;li&gt;Make it executable.
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mv&lt;/span&gt; .git/hooks/pre-commit.sample .git/hooks/pre-commit
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x .git/hooks/pre-commit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;chmod +x&lt;/code&gt; is not optional. Git runs hooks as external processes — if the script isn't executable, Git silently ignores it. No error. No warning. Nothing. Just you, a script that should be running, and a growing suspicion that you've somehow broken your entire Git installation when the actual problem is one missing permission bit.&lt;/p&gt;

&lt;p&gt;The script can be written in any language available on your system. The shebang tells Git which interpreter to use:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Hook: pre-commit — reject commits with TODO in staged files&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; | xargs &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-l&lt;/span&gt; &lt;span class="s1"&gt;'TODO:'&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Error: staged files contain TODO comments."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The client hooks you'll actually use
&lt;/h2&gt;

&lt;p&gt;Client hooks run on the developer's machine during the commit and push lifecycle.&lt;/p&gt;

&lt;h3&gt;
  
  
  pre-commit
&lt;/h3&gt;

&lt;p&gt;Runs before Git creates the commit, right when your files are staged. The natural moment for linting and format checks.&lt;/p&gt;

&lt;p&gt;Exit &lt;code&gt;1&lt;/code&gt; → commit aborted.&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Run ESLint only on staged JS/TS files&lt;/span&gt;
&lt;span class="nv"&gt;staged&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--name-only&lt;/span&gt; &lt;span class="nt"&gt;--diff-filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;ACM | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'\.\(js\|ts\|jsx\|tsx\)$'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$staged&lt;/span&gt;&lt;span class="s2"&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;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$staged&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | xargs npx eslint
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"ESLint found errors. Fix them before committing."&lt;/span&gt;
    &lt;span class="nb"&gt;exit &lt;/span&gt;1
  &lt;span class="k"&gt;fi
fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  commit-msg
&lt;/h3&gt;

&lt;p&gt;Runs after the user writes the commit message but before Git saves it. Receives the path to the temporary file holding the message as its first argument.&lt;/p&gt;

&lt;p&gt;Built for format validation:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Validate Conventional Commits format&lt;/span&gt;
&lt;span class="nv"&gt;commit_msg&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;pattern&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"^(feat|fix|docs|style|refactor|test|chore|perf|build|ci|revert)(&lt;/span&gt;&lt;span class="se"&gt;\(&lt;/span&gt;&lt;span class="s2"&gt;.+&lt;/span&gt;&lt;span class="se"&gt;\)&lt;/span&gt;&lt;span class="s2"&gt;)?: .{1,72}"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$commit_msg&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-qP&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pattern&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Commit message doesn't follow Conventional Commits format."&lt;/span&gt;
  &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Example: feat(auth): add OAuth2 support"&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  prepare-commit-msg
&lt;/h3&gt;

&lt;p&gt;Runs before the editor opens, after Git has generated the default message. Good for automatically injecting context into the message — like the issue number from the branch name:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Prepend issue number from branch name (e.g., feature/GH-123)&lt;/span&gt;
&lt;span class="nv"&gt;branch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;git symbolic-ref &lt;span class="nt"&gt;--short&lt;/span&gt; HEAD&lt;span class="si"&gt;)&lt;/span&gt;
&lt;span class="nv"&gt;issue&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$branch&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'GH-\d+'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$issue&lt;/span&gt;&lt;span class="s2"&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;then
  &lt;/span&gt;&lt;span class="nb"&gt;sed&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"1s/^/[&lt;/span&gt;&lt;span class="nv"&gt;$issue&lt;/span&gt;&lt;span class="s2"&gt;] /"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Working on &lt;code&gt;feature/GH-456-login-form&lt;/code&gt;? Every commit automatically starts with &lt;code&gt;[GH-456]&lt;/code&gt; without you typing it.&lt;/p&gt;

&lt;h3&gt;
  
  
  pre-push
&lt;/h3&gt;

&lt;p&gt;Runs before commits are sent to the remote. The last checkpoint before someone else sees your work:&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;#!/bin/bash&lt;/span&gt;
&lt;span class="c"&gt;# Run test suite before push&lt;/span&gt;
npm &lt;span class="nb"&gt;test
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="nv"&gt;$?&lt;/span&gt; &lt;span class="nt"&gt;-ne&lt;/span&gt; 0 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Tests failed. Push aborted."&lt;/span&gt;
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi
&lt;/span&gt;&lt;span class="nb"&gt;exit &lt;/span&gt;0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ A slow &lt;code&gt;pre-push&lt;/code&gt; hook turns every &lt;code&gt;git push&lt;/code&gt; into a waiting game. If the suite takes ten minutes, keep it for CI and use the hook only for fast unit tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Server-side hooks
&lt;/h2&gt;

&lt;p&gt;Server hooks run on the remote repository — your own Git server, self-hosted GitLab, or similar setups. GitHub and Bitbucket Cloud don't allow arbitrary server-side hooks.&lt;/p&gt;

&lt;p&gt;The relevant ones:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Hook&lt;/th&gt;
&lt;th&gt;When it runs&lt;/th&gt;
&lt;th&gt;Common use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;pre-receive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Before accepting any push&lt;/td&gt;
&lt;td&gt;Reject pushes to protected branches&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;update&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Once per branch being updated&lt;/td&gt;
&lt;td&gt;Per-branch validation rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;post-receive&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;After accepting the full push&lt;/td&gt;
&lt;td&gt;Notifications, deploys, webhooks&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're on GitHub, GitLab.com, or Bitbucket, these capabilities are already available as branch protection rules and webhooks — no need to write them from scratch. Hand-written server hooks are the territory of teams running their own Git infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gotcha everyone discovers at the worst moment
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;.git/hooks/&lt;/code&gt; is not tracked.&lt;/p&gt;

&lt;p&gt;The hook you spent twenty minutes writing, testing, and tweaking lives only on your machine. The person who clones the repository tomorrow gets nothing — no hooks, no indication hooks should exist, nothing. Each new developer starts with a clean &lt;code&gt;.git/hooks/&lt;/code&gt; full of &lt;code&gt;.sample&lt;/code&gt; files and no knowledge of what they're missing.&lt;/p&gt;

&lt;p&gt;Git doesn't share hooks automatically because &lt;code&gt;.git/&lt;/code&gt; is intentionally local — same as &lt;code&gt;.git/config&lt;/code&gt;, which stores your personal settings without pushing them to the remote.&lt;/p&gt;

&lt;p&gt;If your reaction to this is "so what exactly is the point of hooks if the team doesn't have them?"... that's exactly the right question. The answer is below, and it's not "accept the chaos and keep leaving PR comments about the linter."&lt;/p&gt;

&lt;h2&gt;
  
  
  Sharing hooks across the team
&lt;/h2&gt;

&lt;p&gt;There are a few approaches. The right one depends on the project.&lt;/p&gt;

&lt;h3&gt;
  
  
  git config core.hooksPath
&lt;/h3&gt;

&lt;p&gt;The most direct option: store hooks in a versioned directory and tell Git to look 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="c"&gt;# Create a versioned hooks directory&lt;/span&gt;
&lt;span class="nb"&gt;mkdir&lt;/span&gt; .githooks

&lt;span class="c"&gt;# Write your hook&lt;/span&gt;
&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .githooks/pre-commit &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/bash
# hook content here
exit 0
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x .githooks/pre-commit

&lt;span class="c"&gt;# Tell Git to use this directory&lt;/span&gt;
git config core.hooksPath .githooks
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The catch: every developer who clones the repo has to run that &lt;code&gt;git config&lt;/code&gt; manually. Document it in the README and hope people read it, or automate it with one of the tools below.&lt;/p&gt;

&lt;h3&gt;
  
  
  Husky (Node.js projects)
&lt;/h3&gt;

&lt;p&gt;If the project has a &lt;code&gt;package.json&lt;/code&gt;, &lt;a href="https://typicode.github.io/husky/" rel="noopener noreferrer"&gt;Husky&lt;/a&gt; is the standard. The idea is clean: hooks that install automatically on &lt;code&gt;npm install&lt;/code&gt;, just like any other dev dependency. Someone solved the distribution problem, packaged it in 2 KB with zero dependencies, and it runs in ~1ms. 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;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; husky
npx husky init
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This creates a &lt;code&gt;.husky/&lt;/code&gt; directory with a sample &lt;code&gt;pre-commit&lt;/code&gt; hook. Every hook you put there is versioned in the repo and auto-installed the next time anyone runs &lt;code&gt;npm install&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.husky/
  pre-commit     # ← versioned, executable, auto-installed
  commit-msg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As of v9 (current: 9.1.7), configuration is minimal — just a &lt;code&gt;prepare&lt;/code&gt; script in &lt;code&gt;package.json&lt;/code&gt;:&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;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prepare"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"husky"&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;For Node.js projects, this is the lowest-friction path.&lt;/p&gt;

&lt;h3&gt;
  
  
  pre-commit (any language)
&lt;/h3&gt;

&lt;p&gt;For projects without Node — or when you want access to a catalog of ready-made hooks — &lt;a href="https://pre-commit.com/" rel="noopener noreferrer"&gt;pre-commit&lt;/a&gt; is the most popular alternative.&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;pre-commit

&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
apt &lt;span class="nb"&gt;install &lt;/span&gt;pre-commit

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

&lt;/div&gt;



&lt;p&gt;Configuration goes in &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt;, which gets versioned:&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;repos&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/pre-commit/pre-commit-hooks&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;v5.0.0&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;trailing-whitespace&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;end-of-file-fixer&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;check-yaml&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;repo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://github.com/psf/black&lt;/span&gt;
    &lt;span class="na"&gt;rev&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;25.3.0&lt;/span&gt;
    &lt;span class="na"&gt;hooks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;black&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Activate it locally once:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;After that, the &lt;code&gt;pre-commit&lt;/code&gt; hook runs on every &lt;code&gt;git commit&lt;/code&gt;. Anyone cloning the repo runs &lt;code&gt;pre-commit install&lt;/code&gt; once, and &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; tells them exactly which hooks run and at which version.&lt;/p&gt;

&lt;p&gt;The advantage over hand-written scripts: you reuse hooks from public repositories — linters, formatters, security scanners — without writing them yourself. The pre-commit ecosystem has hooks for most languages.&lt;/p&gt;

&lt;h3&gt;
  
  
  lefthook (fast, no runtime dependency)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://github.com/evilmartians/lefthook" rel="noopener noreferrer"&gt;Lefthook&lt;/a&gt; is worth knowing: written in Go, zero runtime dependencies, and can run hooks in parallel.&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;lefthook

&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
apt &lt;span class="nb"&gt;install &lt;/span&gt;lefthook

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

&lt;/div&gt;



&lt;p&gt;Configuration in &lt;code&gt;lefthook.yml&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;pre-commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;parallel&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;lint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.{js,ts}"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npx eslint {staged_files}&lt;/span&gt;
    &lt;span class="na"&gt;format&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;glob&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;*.py"&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;black {staged_files}&lt;/span&gt;

&lt;span class="na"&gt;commit-msg&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;validate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;commitlint --edit {1}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Advantage over Husky: works equally well in non-Node projects. Advantage over pre-commit: faster on large repos because it doesn't spin up isolated Python environments per hook.&lt;/p&gt;

&lt;p&gt;Node.js project? Use Husky. Everything else? Use pre-commit or lefthook. Want to wire it up manually with &lt;code&gt;core.hooksPath&lt;/code&gt;? That works too. All three are valid — the choice comes down to what infrastructure you already have.&lt;/p&gt;

&lt;h2&gt;
  
  
  A complete example
&lt;/h2&gt;

&lt;p&gt;A team wants linting and commit message validation on a Node.js project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-dev&lt;/span&gt; husky lint-staged

npx husky init

&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; .husky/commit-msg &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
#!/bin/sh
npx --no -- commitlint --edit "&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="sh"&gt;"
&lt;/span&gt;&lt;span class="no"&gt;EOF
&lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x .husky/commit-msg
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;lint-staged&lt;/code&gt; runs linters only against staged files, not the entire project:&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;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"prepare"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"husky"&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;"lint-staged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"*.{js,ts}"&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="s2"&gt;"eslint --fix"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"prettier --write"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"*.{css,md}"&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="s2"&gt;"prettier --write"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"devDependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"husky"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^9.1.7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"lint-staged"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"^15.0.0"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;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;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .husky/pre-commit&lt;/span&gt;
&lt;span class="c"&gt;#!/bin/sh&lt;/span&gt;
npx lint-staged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every &lt;code&gt;git commit&lt;/code&gt; now runs ESLint and Prettier on staged files. Something fails → commit aborted. Every developer on the team has the same hooks from the first &lt;code&gt;npm install&lt;/code&gt;. The "have you run the linter?" question disappears from pull request reviews, because the answer is structurally yes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bypassing a hook
&lt;/h2&gt;

&lt;p&gt;When it's urgent — and it always is — the &lt;code&gt;--no-verify&lt;/code&gt; flag skips all client hooks for a single operation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"hotfix: production is on fire"&lt;/span&gt; &lt;span class="nt"&gt;--no-verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't disable hooks permanently. It skips them once. Useful for genuine emergencies.&lt;/p&gt;

&lt;p&gt;If you find yourself using it regularly, the hooks are either too slow or too strict for the team's actual workflow. Fix that before &lt;code&gt;--no-verify&lt;/code&gt; becomes the first thing everyone types after &lt;code&gt;git commit&lt;/code&gt;.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A Git hook is a script that runs automatically at specific points in the Git lifecycle.&lt;/li&gt;
&lt;li&gt;Hooks live in &lt;code&gt;.git/hooks/&lt;/code&gt; and require execute permissions.&lt;/li&gt;
&lt;li&gt;Exit &lt;code&gt;0&lt;/code&gt; = operation continues. Any other exit code = operation aborted.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.git/hooks/&lt;/code&gt; is not versioned — hooks are local by default.&lt;/li&gt;
&lt;li&gt;To share hooks with the team: &lt;code&gt;core.hooksPath&lt;/code&gt;, Husky (Node), pre-commit, or lefthook.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--no-verify&lt;/code&gt; bypasses client hooks for a single operation.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Hooks solve a specific class of problem: the one where the process depends on someone remembering to run the linter. A ten-line script is more reliable than any number of good intentions and any number of pull request comments that say "hey, could you run the formatter before merging?" Not because the team isn't capable — because nobody should have to remember this in the first place.&lt;/p&gt;

&lt;p&gt;Next up: &lt;strong&gt;Git worktrees&lt;/strong&gt;, which let you have multiple working directories of the same repository active at the same time — useful for the moment you're mid-feature and an urgent fix lands in your lap and you'd rather not stash everything and hope nothing breaks.&lt;/p&gt;

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

</description>
      <category>git</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Git submodules: repositories inside repositories without losing your mind</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Tue, 15 Sep 2026 15:57:08 +0000</pubDate>
      <link>https://dev.to/fj_palacios/git-submodules-repositories-inside-repositories-without-losing-your-mind-cg9</link>
      <guid>https://dev.to/fj_palacios/git-submodules-repositories-inside-repositories-without-losing-your-mind-cg9</guid>
      <description>&lt;p&gt;You have a project. Inside that project, you need another project. But you don't want to copy it by hand, because every future update will turn into archaeology with &lt;code&gt;diff&lt;/code&gt;. You also don't want a normal package dependency, because maybe it's an internal template, a private library, shared documentation, or a theme with its own history.&lt;/p&gt;

&lt;p&gt;Then someone says: "use a submodule".&lt;/p&gt;

&lt;p&gt;Silence. What does that even mean? Is it a dependency? A folder? Another repository? Why is there suddenly a &lt;code&gt;.gitmodules&lt;/code&gt; file staring at you like you summoned something ancient? Good news: submodules are not dark magic. Bad news: they are one of those Git features that punish you if you treat them like a normal directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a Git submodule is
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;submodule&lt;/strong&gt; is a Git repository embedded inside another Git repository. The main repository is called the &lt;strong&gt;superproject&lt;/strong&gt;, which sounds like a comic-book villain but simply means "the repo that contains the other one".&lt;/p&gt;

&lt;p&gt;Here is what trips most people up: you see a folder. Git sees a pointer. The main repo does not store the submodule files as its own — it records a note that says: &lt;strong&gt;use this other repository, at exactly this commit&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It's saying:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"My project uses &lt;code&gt;vendor/theme&lt;/code&gt;, but not just any version of &lt;code&gt;vendor/theme&lt;/code&gt;: exactly commit &lt;code&gt;a1b2c3d&lt;/code&gt;."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That part matters. A submodule does not point to "the latest version" by default. It points to an exact commit. Git is being strict, yes, but for a reason: it wants anyone cloning your project to get the same state, not "whatever happens to be on &lt;code&gt;main&lt;/code&gt; today, good luck in production".&lt;/p&gt;

&lt;p&gt;If you read the previous lesson on &lt;a href="https://dev.to/en/tutorials/releasing-new-versions-of-our-projects-using-git-tags"&gt;Git tags&lt;/a&gt;, this should feel familiar: Git is very good at pointing to precise places in history. With a submodule, instead of marking a point in your own repo, you're saying which exact point of another repo belongs to this project.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use submodules and when to run away
&lt;/h2&gt;

&lt;p&gt;Submodules have a reputation. Some of it is deserved. Some of it comes from people using them to solve problems that were never submodule problems.&lt;/p&gt;

&lt;p&gt;Use them when you need to keep two repositories separate, with separate histories, while one still needs to live inside the other:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A shared theme used by multiple static sites.&lt;/li&gt;
&lt;li&gt;An internal library that is not published to a package registry.&lt;/li&gt;
&lt;li&gt;Common documentation reused by several projects.&lt;/li&gt;
&lt;li&gt;A repository of assets or examples that you want pinned to a specific version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not use them as your first option for normal dependencies. If you're in Node, Python, Ruby, Go, or any ecosystem with a decent package manager, use the package manager. That's what it's for. Using submodules where &lt;code&gt;npm install&lt;/code&gt; would do is like bringing an excavator to plant basil.&lt;/p&gt;

&lt;p&gt;Also avoid them if the team doesn't understand the cost: every submodule is another repository to clone, update, review, push, and coordinate. One or two can be reasonable. Twelve nested submodules are a creative way to turn onboarding into an escape room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adding a submodule
&lt;/h2&gt;

&lt;p&gt;Let's use a realistic example: you have a website and want to add a shared theme under &lt;code&gt;vendor/site-theme&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;git submodule add https://github.com/example/site-theme.git vendor/site-theme
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git clones that repository into the path you provided:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Cloning into '/home/dev/my-site/vendor/site-theme'...
remote: Enumerating objects: 42, done.
remote: Counting objects: 100% (42/42), done.
remote: Compressing objects: 100% (31/31), done.
Receiving objects: 100% (42/42), done.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now check the status:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Changes to be committed:
&lt;/span&gt;&lt;span class="gp"&gt;  (use "git restore --staged &amp;lt;file&amp;gt;&lt;/span&gt;...&lt;span class="s2"&gt;" to unstage)
&lt;/span&gt;&lt;span class="go"&gt;    new file:   .gitmodules
    new file:   vendor/site-theme
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the first "wait, what just happened?" moment. Git did not add every file from &lt;code&gt;vendor/site-theme&lt;/code&gt; to the main repository. It added two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;.gitmodules&lt;/code&gt;, with the submodule configuration.&lt;/li&gt;
&lt;li&gt;A special entry for &lt;code&gt;vendor/site-theme&lt;/code&gt;, pointing to a specific commit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;.gitmodules&lt;/code&gt; file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[submodule "vendor/site-theme"]&lt;/span&gt;
    &lt;span class="py"&gt;path&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;vendor/site-theme&lt;/span&gt;
    &lt;span class="py"&gt;url&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;https://github.com/example/site-theme.git&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This file is versioned like any other file. When someone else clones your project, Git reads it to know where the submodule lives and where to fetch it from.&lt;/p&gt;

&lt;p&gt;Run the diff with the submodule flag and you will see what Git is actually recording:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff &lt;span class="nt"&gt;--cached&lt;/span&gt; &lt;span class="nt"&gt;--submodule&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Submodule vendor/site-theme 0000000...a1b2c3d (new submodule)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;a1b2c3d&lt;/code&gt; is the submodule commit your main project is recording. It is not recording "the folder". It is recording "this folder must be at this commit".&lt;/p&gt;

&lt;p&gt;Commit the change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"feat: add shared site theme submodule"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Cloning a project with submodules
&lt;/h2&gt;

&lt;p&gt;This is the part that gets almost everyone the first time.&lt;/p&gt;

&lt;p&gt;You clone a project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/example/my-site.git
&lt;span class="nb"&gt;cd &lt;/span&gt;my-site
&lt;span class="nb"&gt;ls &lt;/span&gt;vendor/site-theme
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the folder is empty. Or almost empty. If you're anything like me when I started, your first instinct was &lt;code&gt;git status&lt;/code&gt; followed by staring at the ceiling. Git did not break. It did exactly what you asked: clone the main repository, nothing else.&lt;/p&gt;

&lt;p&gt;The painless way is to clone like this from the start:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone &lt;span class="nt"&gt;--recurse-submodules&lt;/span&gt; https://github.com/example/my-site.git
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That makes Git clone the main repo, read &lt;code&gt;.gitmodules&lt;/code&gt;, initialize the submodules, and check them out at the correct commits.&lt;/p&gt;

&lt;p&gt;If you already cloned and forgot the flag — because you're human, not a CI pipeline with repressed feelings — run this from the project root:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git submodule update &lt;span class="nt"&gt;--init&lt;/span&gt; &lt;span class="nt"&gt;--recursive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each part does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;update&lt;/code&gt; checks out the submodule commit expected by the superproject.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--init&lt;/code&gt; initializes submodules not yet registered in your local &lt;code&gt;.git/config&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--recursive&lt;/code&gt; does the same for submodules inside submodules.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yes, submodules inside submodules. This is where Git reminds you that you can technically build a Russian doll out of repositories. That doesn't mean you should.&lt;/p&gt;

&lt;h2&gt;
  
  
  Understanding submodule status
&lt;/h2&gt;

&lt;p&gt;The basic command is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git submodule status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Normal output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; a1b2c3d4e5f678901234567890abcdef12345678 vendor/site-theme (v1.2.0)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The hash is the commit currently checked out inside the submodule. The path is where it lives. The bit in parentheses, when present, comes from &lt;code&gt;git describe&lt;/code&gt; and usually shows a nearby tag or name.&lt;/p&gt;

&lt;p&gt;The important part is the first character:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Prefix&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;space&lt;/td&gt;
&lt;td&gt;The submodule is initialized and matches the expected commit.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;-&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The submodule is not initialized. The folder may be empty.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;+&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The submodule is checked out at a different commit than the superproject expects.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;U&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The submodule has merge conflicts. Fun, but not the kind you asked for.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The &lt;code&gt;+&lt;/code&gt; prefix is the one that confuses people most at first. It means: "inside the submodule you have one commit checked out, but the main repo expects another". That is not necessarily wrong. It may be exactly what you want if you're updating the submodule. But until you run &lt;code&gt;git add vendor/site-theme&lt;/code&gt; in the main repo and commit it, that change is not recorded.&lt;/p&gt;

&lt;p&gt;To see the change in a more human form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff &lt;span class="nt"&gt;--submodule&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Submodule vendor/site-theme a1b2c3d..f6e7d8c:
&lt;/span&gt;&lt;span class="gp"&gt;  &amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Improve button spacing
&lt;span class="gp"&gt;  &amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;Fix dark mode contrast
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Much better than staring at two hex strings hoping one of them eventually starts making sense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updating a submodule
&lt;/h2&gt;

&lt;p&gt;Imagine the shared theme has moved forward. There are new commits in &lt;code&gt;site-theme&lt;/code&gt;, and you want your project to use one of them.&lt;/p&gt;

&lt;p&gt;You can enter the submodule and update it like any Git repo:&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;vendor/site-theme
git fetch
git switch main
git pull
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then go back to the main repo:&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; ../..
git status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;Changes not staged for commit:
    modified:   vendor/site-theme (new commits)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That message means: "the submodule now points to another commit". To save that update in the superproject:&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 vendor/site-theme
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: update site theme submodule"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A more direct option is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git submodule update &lt;span class="nt"&gt;--remote&lt;/span&gt; vendor/site-theme
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This fetches changes from the submodule remote and moves it to the configured remote branch. By default, that is usually the remote &lt;code&gt;HEAD&lt;/code&gt; branch. If your team wants the submodule to follow a specific branch, record it:&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 &lt;span class="nt"&gt;-f&lt;/span&gt; .gitmodules submodule.vendor/site-theme.branch main
git submodule update &lt;span class="nt"&gt;--remote&lt;/span&gt; vendor/site-theme
git add .gitmodules vendor/site-theme
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: track main branch for site theme submodule"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't stress about this part if you're just starting. The practical rule is: &lt;strong&gt;updating the submodule changes the commit the main repo points to&lt;/strong&gt;. That change must be added and committed from the main repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Working inside a submodule
&lt;/h2&gt;

&lt;p&gt;Here's where it gets interesting. And by "interesting" I mean "this is where people lose an afternoon".&lt;/p&gt;

&lt;p&gt;When you run &lt;code&gt;git submodule update&lt;/code&gt;, Git usually leaves the submodule in &lt;strong&gt;detached HEAD&lt;/strong&gt;. We've already seen detached HEAD is not the end of the world, but it's also not the place where you want to happily develop like nothing can go wrong.&lt;/p&gt;

&lt;p&gt;If you need to modify the submodule, enter it and switch to a branch:&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;vendor/site-theme
git switch main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make changes, commit, and push inside the submodule:&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 styles/buttons.css
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"fix: improve button contrast"&lt;/span&gt;
git push origin main
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then return to the main repo and record the new submodule commit:&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; ../..
git add vendor/site-theme
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: update site theme submodule pointer"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ol&gt;
&lt;li&gt;Commit inside the submodule.&lt;/li&gt;
&lt;li&gt;Push the submodule.&lt;/li&gt;
&lt;li&gt;Commit the pointer in the main repo.&lt;/li&gt;
&lt;li&gt;Push the main repo.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you do steps 3 and 4 without pushing the submodule, your main repo will point to a commit that only exists on your machine. For your teammates, that is like receiving a treasure map where the island doesn't exist.&lt;/p&gt;

&lt;p&gt;You can ask Git to check this when pushing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;--recurse-submodules&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;check
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If any required submodule commit has not been pushed, Git aborts the push. There is also:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git push &lt;span class="nt"&gt;--recurse-submodules&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;on-demand
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tries to push the required submodules before pushing the main repo. Useful, but use it knowing what it does. Automating things you don't understand is how incidents get proper names.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pulling main repo changes when submodules exist
&lt;/h2&gt;

&lt;p&gt;Another common case: someone on the team updates the submodule and pushes the main repo. You pull:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Git may bring the superproject commit, but your submodule folder can remain at the previous commit. If &lt;code&gt;git status&lt;/code&gt; shows the submodule as modified, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git submodule update &lt;span class="nt"&gt;--init&lt;/span&gt; &lt;span class="nt"&gt;--recursive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command means "put everything where the main repo expects it". It does not blindly update to the latest remote commit; it updates to the exact commit recorded by the superproject.&lt;/p&gt;

&lt;p&gt;If your repo uses submodules often, you can configure Git so many operations recurse automatically:&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 submodule.recurse &lt;span class="nb"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Heads up: &lt;code&gt;git clone&lt;/code&gt; still needs its own &lt;code&gt;--recurse-submodules&lt;/code&gt;. Git had to leave one tiny trap in there. For humility, apparently.&lt;/p&gt;

&lt;h2&gt;
  
  
  Removing a submodule
&lt;/h2&gt;

&lt;p&gt;Removing a submodule should not be &lt;code&gt;rm -rf vendor/site-theme&lt;/code&gt; followed by a short prayer to Linus Torvalds.&lt;/p&gt;

&lt;p&gt;The safe recipe is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git submodule deinit &lt;span class="nt"&gt;-f&lt;/span&gt; vendor/site-theme
git &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; vendor/site-theme
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: remove site theme submodule"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What each command does:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;git submodule deinit -f vendor/site-theme&lt;/code&gt; removes the local submodule checkout and its local configuration.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git rm -f vendor/site-theme&lt;/code&gt; removes the submodule entry from the index and updates &lt;code&gt;.gitmodules&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;commit&lt;/code&gt; records the change for the rest of the team.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Sometimes local metadata may remain under &lt;code&gt;.git/modules/vendor/site-theme&lt;/code&gt;. If you need to clean it manually, be very careful:&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;rm&lt;/span&gt; &lt;span class="nt"&gt;-rf&lt;/span&gt; .git/modules/vendor/site-theme
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read that path again before pressing Enter. &lt;code&gt;rm -rf&lt;/code&gt; has no sense of humor. You do. Your filesystem does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best practices for submodules
&lt;/h2&gt;

&lt;p&gt;Submodules work best when the team treats them as what they are: separate repositories coordinated through a pointer.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Document how to clone the project&lt;/strong&gt; with &lt;code&gt;--recurse-submodules&lt;/code&gt; in the README.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use URLs everyone can access&lt;/strong&gt; in &lt;code&gt;.gitmodules&lt;/code&gt;; your private SSH URL may work on your machine but fail in CI.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not edit submodules in detached HEAD&lt;/strong&gt; if you want to keep the work. Switch to a branch first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Push the submodule before the superproject&lt;/strong&gt; when you've created new commits inside it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Avoid submodules for normal dependencies&lt;/strong&gt; if a reasonable package manager exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do not nest submodules unless there is a real need&lt;/strong&gt;. If you need a diagram to explain how to clone the repo, maybe Git is not the actual problem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A good sign that a submodule is justified: you can explain why that code needs its own repo, its own history, and its own release cadence. If the explanation is "I saw it on Stack Overflow", pause. Breathe. Reconsider.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A submodule is a repository inside another repository.&lt;/li&gt;
&lt;li&gt;The main repo does not store the submodule files: it stores a pointer to a specific commit.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;.gitmodules&lt;/code&gt; records the submodule path and URL, and is versioned with the project.&lt;/li&gt;
&lt;li&gt;To clone with submodules, use &lt;code&gt;git clone --recurse-submodules&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;If you already cloned, use &lt;code&gt;git submodule update --init --recursive&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Updating a submodule changes the commit recorded by the superproject.&lt;/li&gt;
&lt;li&gt;If you work inside the submodule, commit and push there before updating the main repo.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Submodules are useful when you need to coordinate separate repositories without mixing them together. They are not a normal dependency, not a normal folder, and definitely not something you add because it's Friday afternoon and you feel experimental. But when the use case fits, they give you a precise way to say: "this project depends on exactly this other repo, at exactly this commit".&lt;/p&gt;

&lt;p&gt;In the next tutorial we'll look at &lt;strong&gt;Git hooks&lt;/strong&gt;, which let you run scripts automatically at specific moments in the Git workflow: before committing, while validating messages, after receiving changes on a server… useful, powerful, and with enough room for someone to turn &lt;code&gt;git commit&lt;/code&gt; into an obstacle course. We'll take it slowly.&lt;/p&gt;

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

</description>
      <category>git</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Using AI to learn programming concepts</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Mon, 14 Sep 2026 16:54:03 +0000</pubDate>
      <link>https://dev.to/fj_palacios/using-ai-to-learn-programming-concepts-54he</link>
      <guid>https://dev.to/fj_palacios/using-ai-to-learn-programming-concepts-54he</guid>
      <description>&lt;p&gt;You're reading about recursion. The explanation starts fine: “a function that calls itself.” Fair enough. You keep going. There's a factorial example. Still sort of fine. Then suddenly there's a call stack, a base case, return values unwinding in reverse order, and your brain returns &lt;code&gt;HTTP 500&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So you do the reasonable thing: you ask AI.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Explain recursion to me.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the AI gives you a correct definition, a correct example, and a correct analogy. Everything is correct. You still don't get it. Fantastic. We have produced documentation with a positive attitude and actionable insights.&lt;/p&gt;

&lt;p&gt;The problem isn't that AI can't teach. It can. Very well, actually. The problem is that many people use it like a searchable encyclopedia with a chat box, when for learning programming it works much better as a technical tutor: patient, adaptable, and available at 2 AM, which is when questions actually decide to show up. During the day they hide. Like bugs.&lt;/p&gt;

&lt;p&gt;In the previous lesson we looked at &lt;a href="https://dev.to/en/tutorials/advanced-rctf-application"&gt;advanced context, role, task, and format&lt;/a&gt;. Now we'll apply the same idea to something more delicate: learning concepts without settling for an explanation that sounds good but collapses the moment you try to write code.&lt;/p&gt;

&lt;h2&gt;
  
  
  AI as a tutor, not an oracle
&lt;/h2&gt;

&lt;p&gt;AI should not be the divine voice descending from the cloud to hand you truth in Markdown. If you use it that way, you train yourself to accept answers that sound convincing. And as we saw earlier in the course, a convincing answer is not always a correct one.&lt;/p&gt;

&lt;p&gt;For learning, the useful mental model is this: &lt;strong&gt;AI is a tutor you can direct&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That changes the conversation. You don't just ask “explain closures” and passively receive. You tell it what you know, what you don't know, what confuses you, and how you want to test your understanding.&lt;/p&gt;

&lt;p&gt;Look at the difference:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ Explain closures in Python.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That can work, yes. It can also give you a generic explanation that starts well, becomes abstract, and ends with the same &lt;code&gt;outer()&lt;/code&gt; and &lt;code&gt;inner()&lt;/code&gt; example you've seen in twenty blog posts.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Role: act as a Python mentor for someone learning programming.

Context:
I understand variables, functions, and local scope, but I struggle to understand
why an inner function can remember values from an outer function after the outer
function has finished running.

Task:
Explain closures by connecting them to what I already know. Don't use decorators yet.

Format:
1. Short explanation
2. Realistic analogy
3. Minimal Python example
4. Common mistake
5. One question to check whether I understood it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second version doesn't ask for “content.” It asks for a learning experience. It's like going to the doctor: “it hurts” doesn't help much; “it hurts here, since yesterday, when I do this” gives someone something to work with.&lt;/p&gt;

&lt;p&gt;And yes, you can ask questions that feel too basic. You should, actually.&lt;/p&gt;

&lt;h2&gt;
  
  
  “Stupid” questions that are not stupid
&lt;/h2&gt;

&lt;p&gt;One of the best things about using AI to learn is that you can ask without the social pressure of looking lost. No teammate looking over your shoulder. No rushed teacher. No senior sighing because you mixed up &lt;code&gt;map()&lt;/code&gt; and a &lt;code&gt;for&lt;/code&gt; loop. Just you, the question, and a text box.&lt;/p&gt;

&lt;p&gt;Use that.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I know this is basic, but: why does Python use indentation instead of braces?
&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;I don't understand what it means for a variable to be "mutable".
Explain it assuming I know what a list is, but not what happens underneath.
&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;I've read the docs about `return`, but I still confuse returning a value
with printing it to the screen. Can you compare them with small examples?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These questions are gold. Not because they're sophisticated, but because they hit the exact place where your brain is stuck. General documentation can't guess that place.&lt;/p&gt;

&lt;p&gt;If you're following the Python course from scratch, this approach fits concepts like &lt;a href="https://dev.to/en/tutorials/variables-data-types-python"&gt;variables and data types&lt;/a&gt;, &lt;a href="https://dev.to/en/tutorials/defining-functions-python"&gt;functions&lt;/a&gt;, or lists. Don't just ask “what is a list?” Ask about the specific confusion: “why does &lt;code&gt;append()&lt;/code&gt; modify the list but &lt;code&gt;string.upper()&lt;/code&gt; doesn't modify the original string?” That's where real learning starts.&lt;/p&gt;

&lt;p&gt;A good learning question usually 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;I'm learning [concept].
I already understand [what you know].
I'm confused by [specific blocker].
Explain it using [kind of example that helps you].
Then ask me a question to check whether I understood it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No ceremony required. You're not submitting a petition to the International Prompt Committee. You're just making it clear where you are and where you fell over.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Feynman technique with AI
&lt;/h2&gt;

&lt;p&gt;The Feynman technique is brutally simple: if you want to check whether you understand something, try explaining it in your own words. If you can't, you don't understand it yet. If your explanation is hand-wavy, you don't understand it either. And if you explain it using words you couldn't define, your brain has just outsourced the problem.&lt;/p&gt;

&lt;p&gt;With AI, this process has one more participant: someone who doesn't get tired of listening to you explain things badly, doesn't sigh when you take your time, and has exercises ready for when you finish.&lt;/p&gt;

&lt;p&gt;Recommended flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Step 1:
Explain Python generators with a minimal example.

Step 2:
I'll explain it back in my own words. Correct me if there are errors or vague parts:
[your explanation]

Step 3:
Ask me 3 questions to check whether I understood it.
One should be conceptual, one should be code reading, and one should require writing code.

Step 4:
Here are my answers:
[your answers]

Tell me what I understood, what I got wrong, and what I should review.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice the important detail: you're not asking for yet another explanation. You're forcing yourself to produce one. That shift costs something. The kind of cost you feel when you've been reading someone else's code for a while and suddenly have to write your own. But that's exactly where learning starts.&lt;/p&gt;

&lt;p&gt;Example explanation you might send:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I think a generator is a function that doesn't return all values at once.
It uses yield to pause execution and continue later from the same point.
That helps avoid loading a whole list into memory.

I'm not sure whether the generator remembers local variables between calls or
recalculates them each time.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last sentence is the gem: “I'm not sure...” Don't hide it. Explicit doubts are anchors. If you leave them out because you're embarrassed, the AI can't help where it actually matters.&lt;/p&gt;

&lt;p&gt;The dangerous part comes next: AI can correct you incorrectly. Yes, shocking, a probabilistic tool can make things up. We're not looking for faith here; we're looking for initial feedback. If the explanation matters, verify it against official docs, tests, or real code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The “but why?” chain
&lt;/h2&gt;

&lt;p&gt;Some concepts stay blurry because you memorize them too high up. You can repeat the sentence, but you don't know what holds it up.&lt;/p&gt;

&lt;p&gt;“We use functions to avoid repeating code.” Fine. But why is repeating code bad?&lt;/p&gt;

&lt;p&gt;“Because it makes maintenance harder.” Fine. But why?&lt;/p&gt;

&lt;p&gt;“Because if a rule changes, you have to change it in several places.” Fine. And what happens if you forget one?&lt;/p&gt;

&lt;p&gt;Now we're getting somewhere.&lt;/p&gt;

&lt;p&gt;You can use AI to walk down that staircase:&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 truly understand why code duplication is a problem.
Run a "but why" chain with me.
Don't give me a long explanation at the start: ask me, wait for my answer,
and then go deeper based on what I say.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, if you want a single response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Explain why we reduce duplicated code in programming using a 5-level chain:
1. Simple answer
2. Why that answer matters
3. What problem appears in a real project
4. What bug it could cause
5. What design principle sits underneath
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A useful answer might look 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;1. We avoid duplication so we don't write the same thing several times.
2. It matters because each copy can become outdated.
3. In a real project, a duplicated discount rule might be changed in one endpoint
   but not another.
4. That can make two users receive different prices for the same purchase.
5. The principle underneath is DRY: one rule should have one source of truth.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This turns a textbook phrase into a causal chain. And a causal chain is easier to remember because it has weight. It isn't “DRY because reasons.” It's “DRY because if you duplicate business rules, production eventually sends you a bill with interest.”&lt;/p&gt;

&lt;p&gt;AI is especially useful here because it doesn't get tired of your “but why?” A person, by the fourth one, starts looking at the door. AI doesn't. Unfair advantage; use it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The connection game
&lt;/h2&gt;

&lt;p&gt;Learning programming is not collecting loose definitions. It's connecting ideas.&lt;/p&gt;

&lt;p&gt;A Python list is similar to an array in other languages, but not exactly the same. A &lt;code&gt;dict&lt;/code&gt; is like an address book where you look things up by name, but it also has internal rules. A pure function is like an honest vending machine: same input, same output. If only all functions were like that. If only vending machines were too.&lt;/p&gt;

&lt;p&gt;AI can help you create those connections if you ask explicitly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;I'm learning Python lists.
I already understand variables and `for` loops.

Explain lists by connecting them to:
1. An everyday analogy
2. How they work with a `for` loop
3. How they differ from a string
4. A common beginner mistake
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also ask for comparisons between technologies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;How is Django's ORM similar to writing SQL directly?
How is it different?
Explain it for someone who knows basic SELECT, INSERT, and WHERE.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or connections between patterns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Is Python's `@timer` decorator related to the design pattern called Decorator?
Tell me what they share, where they differ, and where people usually get confused.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These questions work because they don't let the concept float in the air. They tie it to something you already know. Learning this way is like building a bridge: if there is no starting bank, it doesn't matter how pretty the bridge is; it ends in the middle of the river. Very poetic for a programming tutorial.&lt;/p&gt;

&lt;p&gt;If you're learning several things at once — say Python, Git, and a bit of Docker — ask for connections between them. AI can help you see that a development environment is not a pile of unrelated tools, but a system where each piece has a job. And if you overdo it until everything seems connected to everything else, don't worry: that's also a phase. It passes. Mostly.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify that you understand, not that you nodded
&lt;/h2&gt;

&lt;p&gt;The danger of learning with AI is that explanations often sound very good. Too good. Clean paragraphs, neat structure, confident tone. Your brain reads that and says: “yes, yes, got it.” Sure it did. Your brain also says “I'll just check one quick thing on YouTube,” and we know how that ends.&lt;/p&gt;

&lt;p&gt;To know whether you understand, you need to produce something.&lt;/p&gt;

&lt;p&gt;Ask for exercises:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Give me 5 progressive exercises about functions in Python.
Don't give me the solutions yet.
Each exercise should check a different concept.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask for code reading:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Show me a small code snippet with lists and loops.
Ask me 4 questions about what it prints and why.
Don't reveal the answer until I respond.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask it to find errors in your reasoning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This is my explanation of the difference between `print()` and `return`:
[your explanation]

Point out errors, incomplete parts, and sentences that sound good but are imprecise.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And above all, ask for variations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Give me a similar exercise, but with a different trap.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sentence is more powerful than it looks. If you only solve an exercise identical to the example, you may have memorized the visual pattern. A variation checks whether you understood the idea.&lt;/p&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;Concept: `return` ends function execution.

Give me 3 code snippets:
1. One where `return` appears inside an `if`
2. One where there is code after `return`
3. One where a function calls another function that returns a value

Ask me what each one prints and why.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is less comfortable than reading another explanation. But learning programming was never just reading. Reading is looking at the map. Solving exercises is walking. And yes, sometimes you step in mud. That's called software development.&lt;/p&gt;

&lt;h2&gt;
  
  
  Be careful: AI can help you learn wrong faster
&lt;/h2&gt;

&lt;p&gt;AI accelerates learning. That sounds good, but there's a flip side: it can also accelerate misunderstandings.&lt;/p&gt;

&lt;p&gt;If it explains a concept incorrectly and you don't verify it, you can internalize a wrong idea with a lot of confidence. Worse: because the explanation was clear, you'll be less likely to suspect it. It's the educational equivalent of a bug with good naming: it looks respectable until it breaks something important.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;If the concept is basic and general, use AI to understand and practice.&lt;/li&gt;
&lt;li&gt;If the concept affects security, concurrency, money, or real data, verify with official documentation.&lt;/li&gt;
&lt;li&gt;If there is code, run it.&lt;/li&gt;
&lt;li&gt;If you can't run the code, at least ask AI to trace execution step by step, then compare it yourself.&lt;/li&gt;
&lt;li&gt;If two answers contradict each other, don't choose the one that sounds more elegant. Investigate.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Useful verification prompt:&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 check this explanation before trusting it.
Look for possible inaccuracies, dangerous simplifications, or cases where it doesn't hold:

[paste explanation]

Format:
1. What is correct
2. What is imprecise
3. Example where the explanation fails or needs nuance
4. Corrected version in 5 lines
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can also ask it to cite documentation, but remember: a made-up citation still looks like a citation. If it matters, open the real docs. Yes, in a browser. I know, very medieval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Template for learning any concept
&lt;/h2&gt;

&lt;p&gt;Save this template and use it when a concept refuses to land:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Role: act as a patient and precise technical mentor.

Context:
I'm learning [concept].
I already understand [previous knowledge].
I'm confused by [specific blocker].
I'm using [language/tool] and want examples in that context.

Task:
Help me understand the concept progressively.
Do not assume I understood it until I can explain it and solve an exercise.

Format:
1. Explanation in 5-7 lines
2. Realistic analogy
3. Minimal example
4. Counterexample or common mistake
5. Comprehension question
6. Small exercise without solution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And after answering the exercise:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This is my solution:
[your solution]

Evaluate it using this format:
1. What is correct
2. What is wrong or incomplete
3. What concept my answer proves I understood
4. What concept I should review
5. A similar exercise with one variation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key is closing the loop: explanation, production, feedback, variation. Without that last part, it's easy to confuse familiarity with understanding. Familiarity is “this rings a bell.” Understanding is “I can use it when the example changes.” Not the same thing, even if your brain tries to sell you the bundle.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Use AI as a directed tutor, not an oracle&lt;/li&gt;
&lt;li&gt;Basic questions are valuable when they name the exact confusion&lt;/li&gt;
&lt;li&gt;The Feynman technique works best when you explain and ask for correction&lt;/li&gt;
&lt;li&gt;The “but why?” chain turns memorized phrases into causal understanding&lt;/li&gt;
&lt;li&gt;The connection game ties new concepts to previous knowledge&lt;/li&gt;
&lt;li&gt;To verify understanding, produce something: explain, read code, write code, or solve exercises&lt;/li&gt;
&lt;li&gt;Clear explanations can still be wrong; verify important claims&lt;/li&gt;
&lt;li&gt;A good learning session ends with feedback and a variation of the exercise&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge:&lt;/strong&gt; Pick a concept that still feels uncomfortable — recursion, closures, mutability, &lt;code&gt;return&lt;/code&gt;, anything — and use the final template. Don't move to the next topic until you've explained the concept in your own words and solved a variation of the exercise.&lt;/p&gt;

&lt;p&gt;Learning with AI is not about receiving faster answers; it's about building a smarter practice system. In the next lesson we'll get into prompts for code generation, and the bar goes up: understanding an explanation won't be enough anymore. You'll need to ask for verifiable code without letting AI turn a small function into a cathedral of boilerplate.&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>Context, role, task, and format: advanced application</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Thu, 10 Sep 2026 14:37:12 +0000</pubDate>
      <link>https://dev.to/fj_palacios/context-role-task-and-format-advanced-application-1c0o</link>
      <guid>https://dev.to/fj_palacios/context-role-task-and-format-advanced-application-1c0o</guid>
      <description>&lt;p&gt;Here's how it goes: you discover RCTF, try it out, and something clicks. The AI stops handing you vague garbage. You're sold on it. Then the next day you ask it something real — a weird bug, a half-built feature, a module that needs structure — and it falls flat. Same framework, same four letters, completely hollow answer.&lt;/p&gt;

&lt;p&gt;Was RCTF a lie? Do you need more acronyms? Are we three lessons away from a full methodology with a keynote, a certification program, and a branded water bottle?&lt;/p&gt;

&lt;p&gt;No. The framework is fine. The problem is that last lesson we used it like a form to fill out. This lesson is about using it the way you'd actually use it at work: with real context, iteration, and formats that give you something you can act on — not just a well-formatted nothing.&lt;/p&gt;

&lt;p&gt;If the framework isn't fresh in your head, go back to &lt;a href="https://dev.to/en/tutorials/the-art-of-the-prompt-core-principles"&gt;the prompt core principles lesson&lt;/a&gt;. This lesson assumes you already know what &lt;strong&gt;role&lt;/strong&gt;, &lt;strong&gt;context&lt;/strong&gt;, &lt;strong&gt;task&lt;/strong&gt;, and &lt;strong&gt;format&lt;/strong&gt; mean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake: treating RCTF like a form
&lt;/h2&gt;

&lt;p&gt;A prompt can include RCTF and still be bad. That's important.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Role: act as a senior backend developer.
Context: I have an API.
Task: help me improve it.
Format: give me a list.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It has all four letters. It also has the precision of a weather report written by someone glancing out the window: “there appears to be sky.”&lt;/p&gt;

&lt;p&gt;The role is too generic. The context doesn't say stack, problem, constraints, or goal. The task has no useful verb. The format asks for a list, but defines no criteria. It's RCTF in name only.&lt;/p&gt;

&lt;p&gt;Now compare it with this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Role: act as a backend engineer specialized in REST APIs with Node.js.

Context: I'm maintaining an Express API that manages orders.
Stack: Node.js 22, Express, PostgreSQL, zod for validation, and Vitest.
Problem: the POST /orders endpoint has too much logic in the controller:
it validates input, calculates discounts, writes to the database, and emits events.
I want to refactor without changing behavior because this is already in production.

Task: propose a refactor plan in small steps. Do not write code yet.

Format:
1. Diagnosis of mixed responsibilities
2. Safe 4-6 step plan
3. Risks for each step
4. Tests I should add before touching code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same structure. Completely different result.&lt;/p&gt;

&lt;p&gt;It's not about length. Each part removes an assumption. And every assumption the AI doesn't have to make is one fewer ticket in the nonsense lottery. Context is almost always where most of them pile up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rich context doesn't mean your life story
&lt;/h2&gt;

&lt;p&gt;Context is the most important part of RCTF, and also the easiest one to ruin. People often jump from “I give no context” to “here's half the company in Markdown.” Neither extreme helps.&lt;/p&gt;

&lt;p&gt;Rich context isn't long context. It's &lt;strong&gt;relevant&lt;/strong&gt; context.&lt;/p&gt;

&lt;p&gt;A good context block answers these questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What are you building?&lt;/li&gt;
&lt;li&gt;What stack or tools are you using?&lt;/li&gt;
&lt;li&gt;What exact area are you touching?&lt;/li&gt;
&lt;li&gt;What constraints must the solution respect?&lt;/li&gt;
&lt;li&gt;What have you already tried?&lt;/li&gt;
&lt;li&gt;What does “good” mean in this case?&lt;/li&gt;
&lt;/ul&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Project context:
I'm working on an e-commerce backend with Django 5 and Django REST Framework.
The database is PostgreSQL 15. We use Redis for cache and Celery for async jobs.
The code has type hints, tests with pytest, and the goal is to avoid breaking
compatibility with the public API.

Relevant domain:
- orders/: Order, OrderItem, Coupon
- products/: Product, Category, Inventory
- users/: CustomUser, Address

Important constraint:
I cannot change the endpoint contract because published mobile apps consume it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This isn't a documentation dump. It's the minimum map the AI needs so it doesn't propose something that breaks production in sentence two.&lt;/p&gt;

&lt;p&gt;Don't overthink it. Keep a base context block for the project and adapt it per task. Think of it like an &lt;code&gt;.env.example&lt;/code&gt; file: not the actual secrets, just enough structure that you're not starting from scratch every time.&lt;/p&gt;

&lt;p&gt;Project context gets you in the building. Code context puts you in the right room.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code context: the bug doesn't live alone
&lt;/h2&gt;

&lt;p&gt;When you ask about a bug, project context helps, but code context does the heavy lifting.&lt;/p&gt;

&lt;p&gt;A decent debugging prompt should include four pieces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;When asking for bug help, include:
1. The relevant code
2. The failing test or reproduction steps
3. The full error message
4. What you expected vs what actually happened
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fourth point sounds obvious until you forget it. The AI can read the error, but it can't infer your intent. “This fails” isn't a diagnosis; it's a scream. Understandable, yes. Useful, less so.&lt;/p&gt;

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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This function breaks for users without email. Fix it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Role: act as a senior Python developer.

Context:
This function normalizes user data imported from CSV. Some users don't have an
email because they come from old records. In that case I want to return None,
not raise an exception.

Code:
````python
def normalize_email(user):
    return user["email"].strip().lower()
````

Error:
KeyError: 'email'

Expected behavior:

- If user["email"] exists and contains text, return it normalized
- If it doesn't exist or is empty, return None
- Keep the function small and easy to test

Task:
Propose the implementation and 3 pytest tests.

Format:

1. Final code
2. Tests
3. Brief explanation of the covered cases

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

&lt;/div&gt;



&lt;p&gt;The AI doesn't get better because it has become “smarter.” It gets better because it no longer has to guess the function contract. You gave it the walls of the room before asking it to arrange the furniture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Iterate: a conversation, not a vending machine
&lt;/h2&gt;

&lt;p&gt;Here's the mental model most people skip: the AI is not a vending machine. You don't drop in a prompt, wait for the answer to fall out, and walk away with your solution. That's how you get responses that technically address the question and solve nothing.&lt;/p&gt;

&lt;p&gt;The next mental shift is this: don't try to solve everything in one giant prompt.&lt;/p&gt;

&lt;p&gt;AI works better when the conversation moves in rounds. The same way you'd work with a person: first define direction, then get into details, then review, correct, and test. You don't tell a teammate: “design, implement, test, document, optimize, and deploy this system; I'll be back in ten minutes.” Well, maybe at some companies. And then things happen.&lt;/p&gt;

&lt;p&gt;An iterative flow could look 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:
Propose a structure for implementing user registration in Flask.
Architecture and files only, no code.

Round 2:
Now implement the User model and validation layer.
Keep the code simple and testable.

Round 3:
Write tests for the happy path and these error cases:
- invalid email
- duplicate email
- password too short

Round 4:
The duplicate email test fails with this error: [paste error]
Diagnose the cause before proposing changes.

Round 5:
Refactor the validation to reduce duplication without changing behavior.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This avoids two classic problems: huge responses that mix incompatible decisions, and changes you can't evaluate because everything happened at once.&lt;/p&gt;

&lt;p&gt;Practical rule: &lt;strong&gt;each round should change one main thing&lt;/strong&gt;. Architecture. Implementation. Tests. Debugging. Refactor. If you mix everything, you won't know which part worked and which part just smuggled a gremlin into your repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Format: ask for a response you can actually use
&lt;/h2&gt;

&lt;p&gt;Format isn't decoration. It's the interface between the AI's response and your workflow.&lt;/p&gt;

&lt;p&gt;If you ask “explain this,” you get an explanation. If you ask “give me a plan with risks and tests,” you get something you can execute. Sounds small, but it's the difference between reading content and moving work forward.&lt;/p&gt;

&lt;p&gt;Useful formats for development:&lt;/p&gt;

&lt;h3&gt;
  
  
  For code generation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Format:
1. Implementation code
2. Example usage
3. Minimal tests
4. Technical decisions and trade-offs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  For debugging
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Format:
1. Likely root cause
2. Why it happens
3. How to confirm it
4. Minimal fix
5. How to prevent it in the future
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  For learning a concept
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Format:
1. One-sentence definition
2. Realistic analogy
3. Minimal code example
4. Common mistake to avoid
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  For comparing options
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Format:
Table with columns:
- Option
- When to use it
- Advantages
- Risks
- Recommendation for my case
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The trick is asking for the format that matches your next action. If you're deciding, ask for a comparison. If you're implementing, ask for steps and tests. If you're learning, ask for an explanation, analogy, and gotcha. The AI doesn't know your next move unless you tell it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Templates you can reuse
&lt;/h2&gt;

&lt;p&gt;Here are three practical templates. Not to copy like scripture, but to adapt.&lt;/p&gt;

&lt;h3&gt;
  
  
  Reviewing code
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Role: act as a senior developer specialized in [language/framework].

Context:
[What the code does]
[Relevant stack]
[Constraints: don't change public API, keep compatibility, etc.]

Code:
[paste code]

Task:
Review the code for readability issues, potential bugs, and edge cases.
Do not refactor yet.

Format:
1. 3-line summary
2. Issues ordered by impact
3. For each issue: why it matters and how you would confirm it
4. Questions that must be answered before touching code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Designing an implementation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Role: act as a pragmatic software engineer.

Context:
[Feature you want to build]
[Stack]
[Constraints]
[What already exists]

Task:
Propose a simple implementation design. Prioritize small, verifiable changes.
Do not write code yet.

Format:
1. Assumptions
2. Proposed design
3. Files you would touch
4. Step-by-step plan
5. Tests needed
6. Risks and alternatives
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Asking for a technical explanation
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Role: act as a technical mentor.

Context:
I'm learning [concept]. I already understand [previous knowledge], but I'm stuck on [specific blocker].

Task:
Explain [concept] by connecting it to what I already know.

Format:
1. Short explanation
2. Analogy
3. Minimal example
4. Counterexample
5. Question to check whether I understood it
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A good template doesn't replace thinking. It removes mental boilerplate so you can focus on the actual problem. Like editor snippets: helpful when you know what you're inserting, dangerous when you fire them blindly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What not to put in context
&lt;/h2&gt;

&lt;p&gt;Less fun, but necessary: not all context should be shared.&lt;/p&gt;

&lt;p&gt;Don't paste:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;API keys&lt;/li&gt;
&lt;li&gt;tokens&lt;/li&gt;
&lt;li&gt;passwords&lt;/li&gt;
&lt;li&gt;real personal data&lt;/li&gt;
&lt;li&gt;company secrets&lt;/li&gt;
&lt;li&gt;full database dumps&lt;/li&gt;
&lt;li&gt;proprietary code if your tool or policy doesn't allow it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Yes, the AI “needs context.” No, it doesn't need your production token. That sentence belongs on a mug, right next to “don't run commands you don't understand.”&lt;/p&gt;

&lt;p&gt;If you need to show data, anonymize it:&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;"user_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user_123"&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;"user@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"plan"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pro"&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;And if the problem depends on a sensitive value, describe it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;The token exists and has a valid JWT format, but the backend returns 401.
I can't share the real token; I can share the anonymized header and claims.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Context quality isn't measured by how many secrets you expose. It's measured by how many assumptions you remove without creating a new problem.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;RCTF doesn't help if each block is generic; each part must reduce assumptions&lt;/li&gt;
&lt;li&gt;Rich context means relevant context, not long context&lt;/li&gt;
&lt;li&gt;For bugs, include code, reproduction, error, and expected behavior&lt;/li&gt;
&lt;li&gt;AI conversations work better in small rounds than in one giant prompt&lt;/li&gt;
&lt;li&gt;Each round should change one main thing&lt;/li&gt;
&lt;li&gt;Format should connect the response to your next action&lt;/li&gt;
&lt;li&gt;Templates help, but they don't replace thinking&lt;/li&gt;
&lt;li&gt;Never share secrets, tokens, or real personal data as “context”&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge:&lt;/strong&gt; Take a real prompt you've used for code help and turn it into a three-round conversation: (1) context and diagnosis, (2) solution proposal, (3) tests or verification. In each round, specify a different format based on what you need to do next.&lt;/p&gt;

&lt;p&gt;RCTF is no longer just a checklist: it's a way to direct a technical conversation. In the next lesson we'll look at how to use AI to learn programming concepts without getting stuck with explanations that sound good but collapse under pressure — because real understanding isn't nodding at a convincing answer, it's being able to spot when someone is selling smoke with nice syntax.&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>Search in Vim: find text without losing your flow</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Wed, 09 Sep 2026 11:33:00 +0000</pubDate>
      <link>https://dev.to/fj_palacios/search-in-vim-find-text-without-losing-your-flow-2npa</link>
      <guid>https://dev.to/fj_palacios/search-in-vim-find-text-without-losing-your-flow-2npa</guid>
      <description>&lt;p&gt;You're inside a 400-line file. You need to find where &lt;code&gt;userId&lt;/code&gt; is used, but your fingers start the classic ritual: &lt;code&gt;j&lt;/code&gt;, &lt;code&gt;j&lt;/code&gt;, &lt;code&gt;j&lt;/code&gt;, &lt;code&gt;j&lt;/code&gt;, look around, &lt;code&gt;j&lt;/code&gt;, &lt;code&gt;j&lt;/code&gt;, &lt;code&gt;k&lt;/code&gt; because you overshot, sigh, repeat. Where was that variable? Why are there three similar blocks? Who wrote this endless function? Ah. You did. Always more fun when the culprit is you.&lt;/p&gt;

&lt;p&gt;Vim doesn't want you wandering through files like you're searching for your keys in the dark — room by room, hand on the wall. It wants you to jump straight to the place. Search isn't a secondary tool: it's a way to move.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two kinds of search
&lt;/h2&gt;

&lt;p&gt;In Vim there are two search families worth separating early:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Search inside the current line&lt;/strong&gt;: &lt;code&gt;f&lt;/code&gt;, &lt;code&gt;F&lt;/code&gt;, &lt;code&gt;t&lt;/code&gt;, &lt;code&gt;T&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search across the file&lt;/strong&gt;: &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;?&lt;/code&gt;, &lt;code&gt;n&lt;/code&gt;, &lt;code&gt;N&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt;, &lt;code&gt;#&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The first helps you move quickly inside one specific line. The second lets you jump between matches across the whole file.&lt;/p&gt;

&lt;p&gt;If you're coming from &lt;a href="https://dev.to/en/tutorials/visual-mode-vim"&gt;Visual mode&lt;/a&gt;, this fits nicely with what you already know: search can become part of a selection. Press &lt;code&gt;v&lt;/code&gt;, search with &lt;code&gt;/&lt;/code&gt;, and Vim extends the selection up to the match. Not magic; just Vim's grammar doing overtime.&lt;/p&gt;

&lt;h2&gt;
  
  
  Searching for characters inside a line
&lt;/h2&gt;

&lt;p&gt;Start with a line like this:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;totalPrice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;calculatePrice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;unitPrice&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Place the cursor at the beginning of the line, on the &lt;code&gt;c&lt;/code&gt; in &lt;code&gt;const&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;f{character}&lt;/code&gt; finds the next character on the same line and places the cursor &lt;strong&gt;on top&lt;/strong&gt; of it:&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="nb"&gt;fp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command jumps to the &lt;code&gt;p&lt;/code&gt; in &lt;code&gt;totalPrice&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;f&lt;/code&gt; stands for &lt;em&gt;find&lt;/em&gt;. Easy enough. For once, Vim didn't call it &lt;code&gt;zq&lt;/code&gt; or something that looks like a router spell.&lt;/p&gt;

&lt;p&gt;The backward version is &lt;code&gt;F{character}&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;F&lt;span class="p"&gt;=&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It searches for &lt;code&gt;=&lt;/code&gt; to the left from wherever you are.&lt;/p&gt;

&lt;p&gt;The important difference comes with &lt;code&gt;t&lt;/code&gt; and &lt;code&gt;T&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;t{character}&lt;/code&gt; searches forward, but stops &lt;strong&gt;right before&lt;/strong&gt; the found character:&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;t&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the line above, that leaves you right before the &lt;code&gt;(&lt;/code&gt; in &lt;code&gt;calculatePrice(...)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;T{character}&lt;/code&gt; does the same thing backward: it searches to the left and stops just after the found character.&lt;/p&gt;

&lt;p&gt;The mental table looks like this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;Direction&lt;/th&gt;
&lt;th&gt;Where the cursor lands&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;f{c}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Forward&lt;/td&gt;
&lt;td&gt;On the character&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;F{c}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Backward&lt;/td&gt;
&lt;td&gt;On the character&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;t{c}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Forward&lt;/td&gt;
&lt;td&gt;Before the character&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;T{c}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Backward&lt;/td&gt;
&lt;td&gt;After the character&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Why does &lt;code&gt;t&lt;/code&gt; exist if we already have &lt;code&gt;f&lt;/code&gt;? Because sometimes you don't want to land on the character; you want to stop right before it so you can operate precisely.&lt;/p&gt;

&lt;p&gt;For example, to delete up to but not including the opening parenthesis:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;dt&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From &lt;code&gt;const totalPrice = calculatePrice...&lt;/code&gt;, that deletes from the cursor up to just before &lt;code&gt;(&lt;/code&gt;. If you used &lt;code&gt;df(&lt;/code&gt;, it would also delete the parenthesis. Sometimes you want the door; sometimes you want the doormat.&lt;/p&gt;

&lt;h2&gt;
  
  
  Repeating character search with ; and ,
&lt;/h2&gt;

&lt;p&gt;After using &lt;code&gt;f&lt;/code&gt;, &lt;code&gt;F&lt;/code&gt;, &lt;code&gt;t&lt;/code&gt;, or &lt;code&gt;T&lt;/code&gt;, repeat the same search with &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;span class="k"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
;
;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Imagine this line:&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="nf"&gt;createUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isActive&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;f,&lt;/code&gt; jumps to the first comma. Each &lt;code&gt;;&lt;/code&gt; jumps to the next comma. Tiny command, constant real-world use.&lt;/p&gt;

&lt;p&gt;To repeat in the opposite direction, use &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;span class="p"&gt;,&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, &lt;code&gt;;&lt;/code&gt; goes forward and &lt;code&gt;,&lt;/code&gt; goes backward. Is it intuitive? Sort of. Will you end up using it without thinking? Also yes. Vim has many things that feel like private jokes at first and later become muscle memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Searching across the file with /
&lt;/h2&gt;

&lt;p&gt;To search forward across the file, use &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;/userId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Press &lt;code&gt;Enter&lt;/code&gt; and Vim jumps to the next occurrence of &lt;code&gt;userId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This changes how you move. Instead of counting lines or hammering &lt;code&gt;j&lt;/code&gt;, you type what you're looking for. If the file is a city, &lt;code&gt;/&lt;/code&gt; is taking the subway. You're not walking door to door checking nameplates.&lt;/p&gt;

&lt;p&gt;To search for the next occurrence, press &lt;code&gt;n&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;span class="k"&gt;n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To go back to the previous occurrence, press &lt;code&gt;N&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;N
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important detail: &lt;code&gt;n&lt;/code&gt; repeats the search in the same direction you originally used. If you searched with &lt;code&gt;/&lt;/code&gt;, &lt;code&gt;n&lt;/code&gt; goes forward. If you searched with &lt;code&gt;?&lt;/code&gt;, &lt;code&gt;n&lt;/code&gt; goes backward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Searching backward with ?
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;?&lt;/code&gt; is like &lt;code&gt;/&lt;/code&gt;, but it searches backward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;?userId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;n&lt;/code&gt; keeps searching backward&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;N&lt;/code&gt; reverses direction and searches forward&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This sounds like a tongue twister until you've used it twice. Short rule: &lt;strong&gt;&lt;code&gt;n&lt;/code&gt; repeats, &lt;code&gt;N&lt;/code&gt; reverses&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you ever press &lt;code&gt;n&lt;/code&gt; and the cursor goes the “wrong” way, Vim isn't being dramatic. The last search probably started with &lt;code&gt;?&lt;/code&gt;. It happens. Breathe, press &lt;code&gt;N&lt;/code&gt;, move on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Searching the word under the cursor with &lt;code&gt;*&lt;/code&gt; and &lt;code&gt;#&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This is one of those commands you should pick up early because it removes a lot of noise.&lt;/p&gt;

&lt;p&gt;Place the cursor on a word 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;Vim searches forward for the next occurrence of that whole word.&lt;/p&gt;

&lt;p&gt;Press &lt;code&gt;#&lt;/code&gt; and it does the same thing backward:&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;Typical case: you're on &lt;code&gt;userId&lt;/code&gt;, press &lt;code&gt;*&lt;/code&gt;, then walk through its uses with &lt;code&gt;n&lt;/code&gt;. No typing the word, no opening a side panel, no reaching for the mouse like you're asking the editor for permission.&lt;/p&gt;

&lt;p&gt;There are also &lt;code&gt;g*&lt;/code&gt; and &lt;code&gt;g#&lt;/code&gt;, which search partial matches. The difference:&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 searches for&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;Whole word forward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;#&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Whole word backward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;g*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Partial match forward&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;g#&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Partial match backward&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you're on &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;*&lt;/code&gt; searches for &lt;code&gt;user&lt;/code&gt; as a whole word. &lt;code&gt;g*&lt;/code&gt; may also find &lt;code&gt;userId&lt;/code&gt;, &lt;code&gt;currentUser&lt;/code&gt;, or &lt;code&gt;users&lt;/code&gt;, depending on the text. Useful, but be careful: searching too broadly is like asking for “some food” and getting the supermarket inventory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search highlighting: hlsearch, incsearch, and noh
&lt;/h2&gt;

&lt;p&gt;When you search, Vim can highlight all matches. That's controlled by &lt;code&gt;hlsearch&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;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nb"&gt;hlsearch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From then on, each search leaves matches highlighted.&lt;/p&gt;

&lt;p&gt;You can also enable &lt;code&gt;incsearch&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;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;set&lt;/span&gt; &lt;span class="nb"&gt;incsearch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;incsearch&lt;/code&gt;, Vim shows matches while you type the pattern. Type &lt;code&gt;/use&lt;/code&gt;, and before pressing &lt;code&gt;Enter&lt;/code&gt; you already see where you'll land. Very useful for avoiding blind searches.&lt;/p&gt;

&lt;p&gt;Now comes the classic Vim moment: you enable &lt;code&gt;hlsearch&lt;/code&gt;, search for something, finish the job, and the file stays highlighted like someone attacked your screen with a marker and too much coffee.&lt;/p&gt;

&lt;p&gt;To clear the current highlight without disabling the option, use:&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;nohlsearch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or the short 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="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;noh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't turn &lt;code&gt;hlsearch&lt;/code&gt; off forever. It only clears the current search; the next search will highlight again. That's what you want most of the time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Case sensitivity and precise searches
&lt;/h2&gt;

&lt;p&gt;You can force a case-insensitive search with &lt;code&gt;\c&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;/userid\&lt;span class="k"&gt;c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That finds &lt;code&gt;userid&lt;/code&gt;, &lt;code&gt;userId&lt;/code&gt;, &lt;code&gt;USERID&lt;/code&gt;, and so on.&lt;/p&gt;

&lt;p&gt;You can force a case-sensitive search with &lt;code&gt;\C&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;/userId\C
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That searches exactly for &lt;code&gt;userId&lt;/code&gt;, respecting uppercase and lowercase letters.&lt;/p&gt;

&lt;p&gt;There are global options like &lt;code&gt;ignorecase&lt;/code&gt; and &lt;code&gt;smartcase&lt;/code&gt;, but you don't need to go there yet. For now, &lt;code&gt;\c&lt;/code&gt; and &lt;code&gt;\C&lt;/code&gt; give you local control when you need it. Not everything needs to become permanent configuration. Yes, even in Vim.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search and replace: first contact
&lt;/h2&gt;

&lt;p&gt;Full substitution deserves its own lesson later, but you need the basics here because substitution is tied closely to search.&lt;/p&gt;

&lt;p&gt;To replace on the current line:&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;s&lt;span class="sr"&gt;/var/&lt;/span&gt;&lt;span class="k"&gt;let&lt;/span&gt;/&lt;span class="k"&gt;g&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That changes every &lt;code&gt;var&lt;/code&gt; to &lt;code&gt;let&lt;/code&gt; on the current line.&lt;/p&gt;

&lt;p&gt;To replace across the whole 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="p"&gt;:&lt;/span&gt;%s&lt;span class="sr"&gt;/var/&lt;/span&gt;&lt;span class="k"&gt;let&lt;/span&gt;/&lt;span class="k"&gt;g&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;%&lt;/code&gt; means “the whole file”. The final &lt;code&gt;g&lt;/code&gt; means “all occurrences on each line”, not just the first one.&lt;/p&gt;

&lt;p&gt;And here's the version you should use when you're not 100% sure:&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;%s&lt;span class="sr"&gt;/var/&lt;/span&gt;&lt;span class="k"&gt;let&lt;/span&gt;/gc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;c&lt;/code&gt; asks for confirmation at each change.&lt;/p&gt;

&lt;p&gt;Global replacements without confirmation are fine when you know exactly what you're doing. When you don't, it's like using a chainsaw to open a cardboard box: technically it works, but you'll spend a while explaining the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Search as motion
&lt;/h2&gt;

&lt;p&gt;The most powerful thing about search in Vim isn't finding text. Every editor can do that. The powerful part is that search is also a &lt;strong&gt;motion&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That means you can combine it with operators from &lt;a href="https://dev.to/en/tutorials/normal-mode-power-of-vim"&gt;Normal mode's 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;/userId
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That deletes from the cursor to the next occurrence of &lt;code&gt;userId&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You can also use searches after entering Visual mode:&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;v&lt;/span&gt;/error
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That selects from the cursor to the next occurrence of &lt;code&gt;error&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This is where Vim stops feeling like a list of isolated commands and starts behaving like a small language: operator + motion + repetition. Search isn't “find text”; it's telling the cursor where the next destination is.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;f{c}&lt;/code&gt; and &lt;code&gt;F{c}&lt;/code&gt; search for a character on the line and land on it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;t{c}&lt;/code&gt; and &lt;code&gt;T{c}&lt;/code&gt; search for a character but stop before or after it&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;;&lt;/code&gt; repeats the last character search; &lt;code&gt;,&lt;/code&gt; repeats it in the opposite direction&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/pattern&lt;/code&gt; searches forward; &lt;code&gt;?pattern&lt;/code&gt; searches backward&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;n&lt;/code&gt; repeats the search; &lt;code&gt;N&lt;/code&gt; reverses direction&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*&lt;/code&gt; and &lt;code&gt;#&lt;/code&gt; search for the word under the cursor forward or backward&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:noh&lt;/code&gt; clears the current highlight without disabling &lt;code&gt;hlsearch&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:%s/old/new/gc&lt;/code&gt; replaces across the file with confirmation&lt;/li&gt;
&lt;li&gt;Search also works as a motion inside Vim's grammar&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Open a real code file and practice without using arrows or &lt;code&gt;j&lt;/code&gt;/&lt;code&gt;k&lt;/code&gt; for long movements. Search for a variable with &lt;code&gt;*&lt;/code&gt;, walk through its uses with &lt;code&gt;n&lt;/code&gt; and &lt;code&gt;N&lt;/code&gt;, jump between commas with &lt;code&gt;f,&lt;/code&gt; and &lt;code&gt;;&lt;/code&gt;, and try a safe replacement with &lt;code&gt;:%s/old/new/gc&lt;/code&gt; using names you can undo without drama.&lt;/p&gt;

&lt;p&gt;You now know how to move through a file without walking line by line. In the next lesson we'll cover &lt;strong&gt;marks and jumps&lt;/strong&gt;: how to leave “bookmarks” inside code and return to previous positions without depending on your memory, which is already busy remembering passwords, commands, and why you opened that browser tab.&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>
    <item>
      <title>Releasing new versions of our projects using Git tags</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Tue, 08 Sep 2026 08:29:47 +0000</pubDate>
      <link>https://dev.to/fj_palacios/releasing-new-versions-of-our-projects-using-git-tags-4b2m</link>
      <guid>https://dev.to/fj_palacios/releasing-new-versions-of-our-projects-using-git-tags-4b2m</guid>
      <description>&lt;p&gt;We're learning a lot of new things to master Git, but until now we haven't said anything about the versions of our projects. We know that you are very advanced in your project and thanks to Git you are working in a more efficient way. You are about to publish the version 0.1 of your project, so people can try it and leave their opinions, but… How the heck we create a new version of our software in Git? Let's go to it!&lt;/p&gt;

&lt;p&gt;In Git there is a function to add &lt;em&gt;tags&lt;/em&gt; to our &lt;em&gt;commits&lt;/em&gt;, which are only a few pointers (like &lt;strong&gt;HEAD&lt;/strong&gt; or each of the branches, as we saw before), this allows us to reference a &lt;em&gt;commit&lt;/em&gt; that we want to be able to locate and access it easily. We can actually create &lt;em&gt;tags&lt;/em&gt; for any &lt;em&gt;commit&lt;/em&gt; we want to locate quickly once time has passed, but it's true that this function is generally used to launch versions of our applications.&lt;/p&gt;

&lt;p&gt;FYI: There are two types of &lt;em&gt;tags&lt;/em&gt;, &lt;em&gt;annotated&lt;/em&gt; and &lt;em&gt;lightweight tags&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Lightweight tags
&lt;/h3&gt;

&lt;p&gt;First we will focus on the &lt;em&gt;lightweight tags&lt;/em&gt;, which are the easiest to learn. This type of &lt;em&gt;tags&lt;/em&gt; is usually used to locate a specially relevant &lt;em&gt;commit&lt;/em&gt; but not a release of a new version. Why? Because you can only add a label with a name and this doesn't seem very useful, isn't it? It's a kind of reminder, usually a temporary one.&lt;/p&gt;

&lt;p&gt;If you want to add the &lt;em&gt;tag&lt;/em&gt; to the most recent &lt;em&gt;commit&lt;/em&gt; just run the &lt;code&gt;git tag v0.1&lt;/code&gt; command, and if we use the &lt;code&gt;git log --oneline --decorate&lt;/code&gt; command we'll see that now our &lt;strong&gt;HEAD&lt;/strong&gt; also has the &lt;strong&gt;tag: v0.1&lt;/strong&gt; pointer; if we want to add a &lt;em&gt;tag&lt;/em&gt; to a previous &lt;em&gt;commit&lt;/em&gt; you can also do it, either through the hash of the &lt;em&gt;commit&lt;/em&gt; (the &lt;em&gt;code&lt;/em&gt; that references the &lt;em&gt;commit&lt;/em&gt; located first in the &lt;code&gt;log&lt;/code&gt; command) or, as we've learned to do, referencing it in a relative way from the most recent &lt;em&gt;commit&lt;/em&gt; (&lt;em&gt;HEAD&lt;/em&gt;), like: &lt;code&gt;git tag v0.1 HEAD~2&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Annotated tags
&lt;/h3&gt;

&lt;p&gt;In simple words, an &lt;em&gt;annotated tag&lt;/em&gt; is something like a &lt;em&gt;commit&lt;/em&gt; as it's treated as an object by Git; in this type of &lt;em&gt;tags&lt;/em&gt; we can add a message that explain the reason for having added that &lt;em&gt;tag&lt;/em&gt;, in addition, the date it was added and who added it will be saved; that's why it's great for our code versioning, since more information is stored and in the message we can write what are the changes that this new version adds.&lt;/p&gt;

&lt;p&gt;The command is very similar to the one we have already learned, but adding some parameters. If we want to write a short message, we can do it as in &lt;em&gt;commits&lt;/em&gt;: &lt;code&gt;git tag -a v0.1 -m "Testing Git tags"&lt;/code&gt; but if you want to write a longer message, which is recommended, just write &lt;code&gt;git tag -a v0.1&lt;/code&gt; so that Git opens us the text editor that we've configured previously to write everything we want. Logically we can also add &lt;em&gt;annotated tags&lt;/em&gt; for previous &lt;em&gt;commits&lt;/em&gt;: &lt;code&gt;git tag -a v0.1 -m "Testing Git tags" HEAD~2&lt;/code&gt; easy peasy, right?&lt;/p&gt;

&lt;h3&gt;
  
  
  Listing tags
&lt;/h3&gt;

&lt;p&gt;We can also get a list of all added &lt;em&gt;tags&lt;/em&gt;, and the easiest way is through the &lt;code&gt;git tag&lt;/code&gt; command. So easy for you guys, huh? Well, what if you already have a mature development and you have many added versions? Holy shit! That list would be very long! But fortunately there's a way to filter the results. If we want to filter the added &lt;em&gt;tags&lt;/em&gt; of the 0.* versions we could execute the &lt;code&gt;git tag -l "v0.*"&lt;/code&gt; command.&lt;/p&gt;

&lt;h3&gt;
  
  
  Getting more information about a tag
&lt;/h3&gt;

&lt;p&gt;If we've added an &lt;em&gt;annotated tag&lt;/em&gt;, with the &lt;code&gt;git show v0.1&lt;/code&gt; command we can see all the information related to the &lt;em&gt;tag&lt;/em&gt; but also with the &lt;em&gt;commit&lt;/em&gt; to which it refers; if we've added a &lt;em&gt;lightweight tag&lt;/em&gt;, then we'll only get information about the &lt;em&gt;commit&lt;/em&gt; to which it refers because from the &lt;em&gt;tag&lt;/em&gt; there would be no information to obtain.&lt;/p&gt;

&lt;h3&gt;
  
  
  Removing tags
&lt;/h3&gt;

&lt;p&gt;At any time we can delete a tag, whether it's an &lt;em&gt;annotated tag&lt;/em&gt; or not, with the same command: &lt;code&gt;git tag -d v0.1&lt;/code&gt;. This command doesn't need more explanation, isn't it?&lt;/p&gt;

&lt;h3&gt;
  
  
  Sending a tag to a remote node
&lt;/h3&gt;

&lt;p&gt;If you want to send a certain &lt;em&gt;tag&lt;/em&gt; to a remote node (if you do not know what a remote is, take a look at &lt;a href="https://dev.to/en/tutorials/creating-our-first-repository-on-github/"&gt;Creating our first repository on GitHub&lt;/a&gt;) you can do it in the same way as we'd send a new branch: &lt;code&gt;git push origin v0.1&lt;/code&gt; and if you want to send more than one &lt;em&gt;tag&lt;/em&gt; you can execute the command &lt;code&gt;git push origin --tags&lt;/code&gt; to send them all with a single command.&lt;/p&gt;

&lt;h3&gt;
  
  
  Working with tags on remote nodes
&lt;/h3&gt;

&lt;p&gt;When sending &lt;em&gt;tags&lt;/em&gt; to remote nodes like &lt;a href="https://github.com/" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; or &lt;a href="https://gitlab.com/" rel="noopener noreferrer"&gt;GitLab&lt;/a&gt; we offer users who visit our projects the information that a new version was released but also that they can download it easily, and for convenience it's created automatically. In GitHub, for example, when a new &lt;em&gt;tag&lt;/em&gt; is sent, it obviously appears in the section of &lt;em&gt;tags&lt;/em&gt;, but also in the &lt;em&gt;releases&lt;/em&gt; one, therefore create a new version of our application so that users can download it is a quick and easy task.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7zkckyts3m9xw8u17a4y.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7zkckyts3m9xw8u17a4y.webp" alt="Tags section on GitHub" width="800" height="204"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now you can no longer say that you've not shared with the world a version of your application (I hope an open source application) because you didn't know how to do it.&lt;/p&gt;

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

</description>
      <category>git</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Defining functions in Python: def, parameters, and return</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Fri, 04 Sep 2026 10:56:54 +0000</pubDate>
      <link>https://dev.to/fj_palacios/defining-functions-in-python-def-parameters-and-return-2ch8</link>
      <guid>https://dev.to/fj_palacios/defining-functions-in-python-def-parameters-and-return-2ch8</guid>
      <description>&lt;p&gt;You copy three lines of code. Then three more. Then the same three again, but with a different number. At first it feels productive. Ten minutes later your file looks like a haunted photocopier: lots of repetition, very little intention, and every tiny change has to be made in four different places because apparently we enjoy suffering.&lt;/p&gt;

&lt;p&gt;That's where functions come in. Not as some academic ritual with fancy terminology, but as a way to tell Python: "this operation has a name; run it whenever I ask."&lt;/p&gt;

&lt;h2&gt;
  
  
  What is a function?
&lt;/h2&gt;

&lt;p&gt;A &lt;strong&gt;function&lt;/strong&gt; is a reusable block of code. You give it a name, put instructions inside it, and later you execute that block by calling the function.&lt;/p&gt;

&lt;p&gt;Think of a coffee machine. You don't want to manually repeat every step each morning: grind beans, heat water, press button, stare into the void while life compiles. You want to press &lt;code&gt;make_coffee()&lt;/code&gt; and let the process happen. A function is that: a name attached to a sequence of steps.&lt;/p&gt;

&lt;p&gt;In the previous lesson you worked with lists and methods like &lt;a href="https://dev.to/en/tutorials/list-methods-python"&gt;&lt;code&gt;append()&lt;/code&gt; and &lt;code&gt;remove()&lt;/code&gt;&lt;/a&gt;. If you think about it, you've already been using functions constantly: &lt;code&gt;print()&lt;/code&gt;, &lt;code&gt;len()&lt;/code&gt;, &lt;code&gt;range()&lt;/code&gt;. The difference now is that you're going to create your own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your first function with def
&lt;/h2&gt;

&lt;p&gt;In Python, you define a function with the &lt;code&gt;def&lt;/code&gt; keyword:&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, Python&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;This &lt;strong&gt;defines&lt;/strong&gt; the function, but it doesn't run it yet. It's like putting a recipe in a drawer: it exists, but nobody is cooking.&lt;/p&gt;

&lt;p&gt;To execute it, call the function by name with parentheses:&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, Python&lt;/span&gt;&lt;span class="sh"&gt;"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;Hello&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Python&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The basic structure looks 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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;function_name&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# Function body
&lt;/span&gt;    &lt;span class="n"&gt;statement_1&lt;/span&gt;
    &lt;span class="n"&gt;statement_2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The &lt;code&gt;def&lt;/code&gt; line ends with &lt;code&gt;:&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;The function body is indented&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you forget the indentation, Python complains. And this time Python is right. Indentation tells Python which code belongs inside the function and which code is outside. It's not decoration; it's syntax.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why functions matter
&lt;/h2&gt;

&lt;p&gt;Look at this code:&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;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;25&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="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;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;40&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="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;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;99&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="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;It works. Duct tape on a leaking pipe also works for a while. That doesn't make it architecture.&lt;/p&gt;

&lt;p&gt;The issue isn't that the code is long; the issue is that the same idea is repeated. If the tax rate changes tomorrow, you have to update it everywhere. Miss one place and congratulations: you've created an accounting bug, which sounds boring until it bites.&lt;/p&gt;

&lt;p&gt;With 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="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="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;span class="nf"&gt;calculate_total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&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;40&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;99&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.25
48.4
119.79
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the logic lives in one place. That's the &lt;strong&gt;DRY&lt;/strong&gt; principle: &lt;em&gt;Don't Repeat Yourself&lt;/em&gt;. Not because repetition is morally wrong, but because repetition multiplies the number of places where bugs can hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parameters and arguments
&lt;/h2&gt;

&lt;p&gt;In this 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="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="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;&lt;code&gt;price&lt;/code&gt; is a &lt;strong&gt;parameter&lt;/strong&gt;. It's the variable the function expects to receive.&lt;/p&gt;

&lt;p&gt;When you call the 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="nf"&gt;calculate_total&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;25&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;25&lt;/code&gt; is an &lt;strong&gt;argument&lt;/strong&gt;. It's the actual value passed into that parameter.&lt;/p&gt;

&lt;p&gt;The distinction sounds like quiz material until you need to explain code precisely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Parameter&lt;/strong&gt;: the name in the function definition&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Argument&lt;/strong&gt;: the value sent when calling the function&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Like a reserved seat versus the person sitting in it. The seat is the parameter; the person is the argument. If you're like me when I started, you'll call everything a parameter for a while. You'll survive. Python won't confiscate your keyboard.&lt;/p&gt;

&lt;p&gt;You can define multiple parameters:&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;show_user&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;age&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="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;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; years old&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;show_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Ana&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;show_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Luis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&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;Ana is 34 years old
Luis is 28 years old
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Order matters: Python assigns the first argument to the first parameter, the second argument to the second parameter, and so on. In the next lesson we'll look at more flexible ways to call functions, but for now keep the simple rule: order wins.&lt;/p&gt;

&lt;h2&gt;
  
  
  return: giving a value back
&lt;/h2&gt;

&lt;p&gt;So far our functions print things. That's fine for seeing output, but useful functions usually &lt;strong&gt;return&lt;/strong&gt; a value so the rest of your program can keep working with it.&lt;/p&gt;

&lt;p&gt;That's what &lt;code&gt;return&lt;/code&gt; is for:&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;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&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;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&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;8
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference between &lt;code&gt;print()&lt;/code&gt; and &lt;code&gt;return&lt;/code&gt; is massive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;print()&lt;/code&gt; displays something on the screen&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;return&lt;/code&gt; gives a value back to the code that called the function&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters. &lt;code&gt;print()&lt;/code&gt; is like saying something out loud. &lt;code&gt;return&lt;/code&gt; is like handing someone the tool they need to continue the job. One creates output; the other lets your program move forward.&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;create_message&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="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;Hello, &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="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;create_message&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Marta&lt;/span&gt;&lt;span class="sh"&gt;"&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;span class="nf"&gt;upper&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, MARTA
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the function returns a string, you can store it, transform it, put it in a list, compare it, whatever you need. If it only printed the string, the value would disappear into the terminal like tears in production logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  return exits the function
&lt;/h2&gt;

&lt;p&gt;When Python reaches a &lt;code&gt;return&lt;/code&gt;, it leaves the function immediately:&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;classify_age&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;age&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;age&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;18&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;minor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;adult&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="nf"&gt;classify_age&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="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;classify_age&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;/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;minor
adult
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;age&lt;/code&gt; is less than &lt;code&gt;18&lt;/code&gt;, Python returns &lt;code&gt;"minor"&lt;/code&gt; and doesn't continue running the rest of the function. This lets you write clearer functions, especially when you want to handle simple cases early and leave the general case at the end.&lt;/p&gt;

&lt;p&gt;Don't stress about patterns yet. For now, understand this: &lt;code&gt;return&lt;/code&gt; doesn't just send a value back; it also marks the exit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Functions that return None
&lt;/h2&gt;

&lt;p&gt;In Python, every function returns something. If you don't write a &lt;code&gt;return&lt;/code&gt;, the function returns &lt;code&gt;None&lt;/code&gt; automatically.&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;name&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="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello, &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="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;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Nora&lt;/span&gt;&lt;span class="sh"&gt;"&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;Hello, Nora
None
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Feeling like "wait, I didn't ask for that &lt;code&gt;None&lt;/code&gt;"? Fair. Python does it anyway.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;None&lt;/code&gt; means "no value here". It's not &lt;code&gt;0&lt;/code&gt;, not &lt;code&gt;""&lt;/code&gt;, not &lt;code&gt;False&lt;/code&gt;. It's its own thing: a special value that represents the absence of a useful result.&lt;/p&gt;

&lt;p&gt;This usually appears when a function performs an effect — like printing to the screen — but doesn't return data for the rest of the program to use. Later, when you start writing larger programs, the difference between "doing something" and "returning something" becomes extremely important.&lt;/p&gt;

&lt;h2&gt;
  
  
  Function names
&lt;/h2&gt;

&lt;p&gt;Function names follow the same convention as variables: &lt;code&gt;snake_case&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;calculate_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;percentage&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;percentage&lt;/span&gt; &lt;span class="o"&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;p&gt;The name should explain what the function does. Don't be mysterious with your future self. Your future self is not an archaeologist looking for a side quest.&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;c&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&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;p&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ❌ Too cryptic
&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate_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;percentage&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;percentage&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ✅ Clear intent
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A good rule: if you have to read the function body to understand why the function exists, the name probably needs work.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical case: simple calculator
&lt;/h2&gt;

&lt;p&gt;Let's build a tiny calculator using functions. No interface, no strange menus, no &lt;code&gt;eval()&lt;/code&gt; — let's not summon demons for sport.&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;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;subtract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;multiply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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;a&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;divide&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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;b&lt;/span&gt; &lt;span class="o"&gt;==&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;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cannot divide by zero&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;a&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;b&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;add&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="mi"&gt;5&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;subtract&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="mi"&gt;5&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;multiply&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="mi"&gt;5&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;divide&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="mi"&gt;5&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;divide&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="mi"&gt;0&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;15
5
50
2.0
Cannot divide by zero
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each function has a clear responsibility. &lt;code&gt;add()&lt;/code&gt; adds. &lt;code&gt;subtract()&lt;/code&gt; subtracts. &lt;code&gt;divide()&lt;/code&gt; divides and protects the problematic case. The code is simple, but it already has structure.&lt;/p&gt;

&lt;p&gt;Could you write all operations one after another instead? Of course. You could also store all your socks in the oven. The question isn't whether it can be done; the question is how much you'll suffer when the program grows.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A function is a reusable block of code with a name&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;def&lt;/code&gt; defines a function; calling the function executes its body&lt;/li&gt;
&lt;li&gt;Parameters live in the definition; arguments are the values sent when calling&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;return&lt;/code&gt; gives a value back and exits the function&lt;/li&gt;
&lt;li&gt;If a function has no &lt;code&gt;return&lt;/code&gt;, it returns &lt;code&gt;None&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;print()&lt;/code&gt; displays information; &lt;code&gt;return&lt;/code&gt; lets you keep using the value&lt;/li&gt;
&lt;li&gt;Function names should use &lt;code&gt;snake_case&lt;/code&gt; and describe intention&lt;/li&gt;
&lt;li&gt;Functions reduce duplication and make code easier to change&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Create four functions: &lt;code&gt;add(a, b)&lt;/code&gt;, &lt;code&gt;subtract(a, b)&lt;/code&gt;, &lt;code&gt;multiply(a, b)&lt;/code&gt;, and &lt;code&gt;divide(a, b)&lt;/code&gt;. Then create a fifth function, &lt;code&gt;show_result(operation, result)&lt;/code&gt;, that prints a message like &lt;code&gt;"Addition: 15"&lt;/code&gt;. Test every operation with several numbers, including division by zero.&lt;/p&gt;

&lt;p&gt;Functions are the first serious step toward modular code. You're no longer writing an endless sequence of instructions; you're naming ideas, separating responsibilities, and building pieces you can reuse. In the next lesson we'll go deeper into default parameters, keyword arguments, &lt;code&gt;*args&lt;/code&gt;, and &lt;code&gt;**kwargs&lt;/code&gt; — the part where functions stop being a door and become a Swiss Army knife.&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>Advanced Dockerfile instructions: ARG, ENV, ENTRYPOINT and more</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Wed, 02 Sep 2026 08:49:57 +0000</pubDate>
      <link>https://dev.to/fj_palacios/advanced-dockerfile-instructions-arg-env-entrypoint-and-more-4o7p</link>
      <guid>https://dev.to/fj_palacios/advanced-dockerfile-instructions-arg-env-entrypoint-and-more-4o7p</guid>
      <description>&lt;p&gt;Most Dockerfiles have a few lines that everyone copies from an example that worked once and never questions again. &lt;code&gt;ENTRYPOINT&lt;/code&gt; and &lt;code&gt;CMD&lt;/code&gt; near the bottom. &lt;code&gt;ARG&lt;/code&gt; and &lt;code&gt;ENV&lt;/code&gt; somewhere near the top. The container starts, the app runs, and nobody asks too many questions.&lt;/p&gt;

&lt;p&gt;Until it breaks. Or until someone on the team notices the API key getting passed through &lt;code&gt;ARG&lt;/code&gt; — which shows up in the image history for anyone to read — or asks why the container ignores the command you pass to &lt;code&gt;docker run&lt;/code&gt;. That conversation should have happened earlier. This lesson is it.&lt;/p&gt;

&lt;h2&gt;
  
  
  ARG vs ENV
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ARG&lt;/code&gt; or &lt;code&gt;ENV&lt;/code&gt;? Which one persists into the container? Which one disappears after the build? Can you combine them? Does the order in the Dockerfile matter? They look similar — both define variables, both accept a name and an optional default value — but they exist at completely different moments in the Docker lifecycle.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ARG&lt;/code&gt;&lt;/strong&gt; defines a variable that only exists during the build process. Once the image is built, it's gone. The container that runs from that image never sees it. Use &lt;code&gt;ARG&lt;/code&gt; for things that affect how the image is built: the Node version, the app version string, a compile-time flag.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ENV&lt;/code&gt;&lt;/strong&gt; defines an environment variable that persists into the running container. Use &lt;code&gt;ENV&lt;/code&gt; for anything your application needs at runtime.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; NODE_VERSION=20&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:${NODE_VERSION}-alpine&lt;/span&gt;

&lt;span class="k"&gt;ARG&lt;/span&gt;&lt;span class="s"&gt; APP_VERSION=1.0.0&lt;/span&gt;
&lt;span class="k"&gt;ENV&lt;/span&gt;&lt;span class="s"&gt; APP_VERSION=${APP_VERSION}&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "index.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;NODE_VERSION&lt;/code&gt; only exists long enough to pick the base image. After that, it's gone. &lt;code&gt;APP_VERSION&lt;/code&gt; is explicitly bridged from &lt;code&gt;ARG&lt;/code&gt; to &lt;code&gt;ENV&lt;/code&gt; — that's how a build-time value makes it into the running container.&lt;/p&gt;

&lt;p&gt;You can override &lt;code&gt;ARG&lt;/code&gt; at build time, and &lt;code&gt;ENV&lt;/code&gt; at runtime:&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;# Override ARG during build&lt;/span&gt;
docker build &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;NODE_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;22 &lt;span class="nt"&gt;-t&lt;/span&gt; my-app &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Override ENV when running&lt;/span&gt;
docker run &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;APP_VERSION&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;2.0.0 my-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The security gotcha you need to know about
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;ARG&lt;/code&gt; is not a safe place for secrets. Values passed through &lt;code&gt;ARG&lt;/code&gt; end up in the image history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nb"&gt;history &lt;/span&gt;my-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;IMAGE         CREATED       CREATED BY
abc123def456  2 hours ago   CMD ["node", "index.js"]
...           ...           ARG API_KEY=s3cr3t          ← right there
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've passed an API key, token, or any credential through &lt;code&gt;ARG&lt;/code&gt;, it's recoverable from the image by anyone who can pull it. This is covered in detail in &lt;a href="https://dev.to/en/tutorials/dockerfile-best-practices-security"&gt;Dockerfile security best practices&lt;/a&gt; — if you skipped that one, it's worth going back.&lt;/p&gt;

&lt;p&gt;The rule: build-time secrets to BuildKit build secrets (next lesson). Runtime secrets to environment variables injected by your secrets management system, never hardcoded in the Dockerfile.&lt;/p&gt;

&lt;h2&gt;
  
  
  ENTRYPOINT vs CMD
&lt;/h2&gt;

&lt;p&gt;If you're like most people when they first encounter this, you've copied those lines from Stack Overflow, seen that sometimes both appear and sometimes only one, and assumed Docker has its quirks. It doesn't. It has concrete rules, and once you understand them everything clicks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;CMD&lt;/code&gt;&lt;/strong&gt; defines the default command a container runs. It's fully replaceable — pass a different command to &lt;code&gt;docker run&lt;/code&gt; and &lt;code&gt;CMD&lt;/code&gt; is ignored entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;ENTRYPOINT&lt;/code&gt;&lt;/strong&gt; defines the executable that always runs when the container starts. You can't override it with a regular argument — you need &lt;code&gt;--entrypoint&lt;/code&gt; explicitly.&lt;/p&gt;

&lt;p&gt;When you use both, &lt;code&gt;CMD&lt;/code&gt; provides the default arguments to &lt;code&gt;ENTRYPOINT&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# CMD only — fully replaceable command&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ubuntu:22.04&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["echo", "hello world"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run my-image               &lt;span class="c"&gt;# → "hello world"&lt;/span&gt;
docker run my-image &lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"bye"&lt;/span&gt;   &lt;span class="c"&gt;# → "bye"  (CMD replaced)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ENTRYPOINT only — fixed executable&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ubuntu:22.04&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["echo"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run my-image               &lt;span class="c"&gt;# → ""  (nothing, no args)&lt;/span&gt;
docker run my-image &lt;span class="s2"&gt;"hello"&lt;/span&gt;      &lt;span class="c"&gt;# → "hello"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# ENTRYPOINT + CMD — the most useful pattern&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; ubuntu:22.04&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["echo"]&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["hello world"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run my-image               &lt;span class="c"&gt;# → "hello world"  (default)&lt;/span&gt;
docker run my-image &lt;span class="s2"&gt;"other text"&lt;/span&gt; &lt;span class="c"&gt;# → "other text"  (CMD replaced)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ENTRYPOINT + CMD&lt;/code&gt; combination is what you want for CLI tools packaged as containers: the tool is fixed, the arguments are defaults you can override without knowing anything about the image internals.&lt;/p&gt;

&lt;p&gt;If you're following along but it's not fully clicking yet, that's fine — this is the combination that gets googled most often by people who've been using Docker for months. The 90% case is exec form in &lt;code&gt;ENTRYPOINT&lt;/code&gt; with &lt;code&gt;CMD&lt;/code&gt; as default arguments. The rest comes with practice.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shell form vs exec form
&lt;/h3&gt;

&lt;p&gt;Both instructions come in two flavors:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# Shell form — runs via /bin/sh -c&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; echo "hello"&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; echo "hello"&lt;/span&gt;

&lt;span class="c"&gt;# Exec form — executes directly, no shell involved&lt;/span&gt;
&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["echo", "hello"]&lt;/span&gt;
&lt;span class="k"&gt;ENTRYPOINT&lt;/span&gt;&lt;span class="s"&gt; ["echo", "hello"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Use exec form for &lt;code&gt;ENTRYPOINT&lt;/code&gt;.&lt;/strong&gt; Shell form runs your process as a child of &lt;code&gt;/bin/sh&lt;/code&gt;, which has two annoying consequences: OS signals like &lt;code&gt;SIGTERM&lt;/code&gt; (sent when stopping a container) don't reach your process because the shell intercepts them, and on minimal images without a shell the container simply won't start. With exec form, your process is PID 1 and receives signals directly.&lt;/p&gt;

&lt;h2&gt;
  
  
  HEALTHCHECK
&lt;/h2&gt;

&lt;p&gt;This is the instruction no one adds until a container fails silently in production and it takes longer than it should to notice. After that, they add it to everything.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;HEALTHCHECK&lt;/code&gt; tells Docker how to verify a container is functioning correctly. Docker runs the specified command on a schedule and updates the container's status: &lt;code&gt;starting&lt;/code&gt;, &lt;code&gt;healthy&lt;/code&gt;, or &lt;code&gt;unhealthy&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; node:20-alpine&lt;/span&gt;
&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; . .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;npm ci
&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;

&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=30s --timeout=10s --start-period=5s --retries=3 \&lt;/span&gt;
  CMD curl -f http://localhost:3000/health || exit 1

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["node", "index.js"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The parameters:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;--interval&lt;/code&gt;: how often the check runs (default: 30s)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--timeout&lt;/code&gt;: if the command takes longer than this, it's a failure (default: 30s)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--start-period&lt;/code&gt;: grace period at startup before failures start counting (default: 0s)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--retries&lt;/code&gt;: how many consecutive failures before marking as &lt;code&gt;unhealthy&lt;/code&gt; (default: 3)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The check command should return &lt;code&gt;0&lt;/code&gt; for healthy and &lt;code&gt;1&lt;/code&gt; for unhealthy. The &lt;code&gt;|| exit 1&lt;/code&gt; ensures that if &lt;code&gt;curl&lt;/code&gt; fails — because the server isn't responding or returns an HTTP error — the check reports failure too.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;CONTAINER ID   IMAGE     STATUS
a1b2c3d4e5f6   my-app    Up 2 minutes (healthy)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;(healthy)&lt;/code&gt; in &lt;code&gt;docker ps&lt;/code&gt; means the HEALTHCHECK is running and passing. Docker Compose and orchestrators use this status to know whether a container is ready to accept traffic, whether it needs to be restarted, and whether to consider a deploy successful.&lt;/p&gt;

&lt;p&gt;If your image doesn't have &lt;code&gt;curl&lt;/code&gt; — Alpine or distroless images — use &lt;code&gt;wget&lt;/code&gt; or a small script in your app's runtime:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="c"&gt;# With wget (Alpine)&lt;/span&gt;
&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=30s --timeout=5s \&lt;/span&gt;
  CMD wget -q --spider http://localhost:3000/health || exit 1

&lt;span class="c"&gt;# With Node.js runtime&lt;/span&gt;
&lt;span class="k"&gt;HEALTHCHECK&lt;/span&gt;&lt;span class="s"&gt; --interval=30s --timeout=5s \&lt;/span&gt;
  CMD node -e "require('http').get('http://localhost:3000/health', r =&amp;gt; process.exit(r.statusCode === 200 ? 0 : 1))"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  VOLUME and EXPOSE
&lt;/h2&gt;

&lt;p&gt;Here's the part nobody documents well because it's genuinely a bit odd: &lt;code&gt;EXPOSE&lt;/code&gt; doesn't expose anything and &lt;code&gt;VOLUME&lt;/code&gt; doesn't mount anything. They're very formal comments that Docker decided to turn into instructions. The naming is optimistic.&lt;/p&gt;

&lt;h3&gt;
  
  
  EXPOSE
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;EXPOSE&lt;/code&gt; declares which port the container listens on. It does not open that port on the host. It does not make the container accessible from outside. It does nothing at runtime. It's executable documentation — it tells anyone reading the Dockerfile what port to publish, and tools like Docker Compose can discover it automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To actually publish the port to the host, you need &lt;code&gt;-p&lt;/code&gt; when 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 run &lt;span class="nt"&gt;-p&lt;/span&gt; 8080:3000 my-app
&lt;span class="c"&gt;# Host port 8080 → container port 3000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;EXPOSE&lt;/code&gt; without &lt;code&gt;-p&lt;/code&gt; or &lt;code&gt;ports:&lt;/code&gt; in Compose is invisible from the outside. Add it anyway — it's good documentation practice and some orchestrators use it for service discovery.&lt;/p&gt;

&lt;h3&gt;
  
  
  VOLUME
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;VOLUME&lt;/code&gt; declares a mount point inside the container. Docker will automatically create an anonymous volume for that path when the container starts, ensuring data persists even if the container is removed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;VOLUME&lt;/span&gt;&lt;span class="s"&gt; ["/app/data", "/app/logs"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What it doesn't do: it doesn't define where on the host the data lives (that's &lt;code&gt;-v&lt;/code&gt; in &lt;code&gt;docker run&lt;/code&gt; or &lt;code&gt;volumes:&lt;/code&gt; in Compose), and it doesn't save you from having to specify the volume if you want control over it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Without specifying — Docker creates an anonymous volume&lt;/span&gt;
docker run my-app

&lt;span class="c"&gt;# Named volume — recommended&lt;/span&gt;
docker run &lt;span class="nt"&gt;-v&lt;/span&gt; my-data:/app/data my-app

&lt;span class="c"&gt;# Bind mount — host directory&lt;/span&gt;
docker run &lt;span class="nt"&gt;-v&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;/data:/app/data my-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The primary use of &lt;code&gt;VOLUME&lt;/code&gt; in a Dockerfile is to document which paths contain data that shouldn't be lost. It also has a practical side effect: any content copied into that path before the &lt;code&gt;VOLUME&lt;/code&gt; instruction is included in the initial volume state. Any &lt;code&gt;COPY&lt;/code&gt; or &lt;code&gt;RUN&lt;/code&gt; that writes to that path after &lt;code&gt;VOLUME&lt;/code&gt; won't behave as expected.&lt;/p&gt;

&lt;p&gt;⚠️ For this reason, put &lt;code&gt;VOLUME&lt;/code&gt; near the end of the Dockerfile, after copying everything that needs to be there.&lt;/p&gt;




&lt;p&gt;With &lt;code&gt;ARG&lt;/code&gt;, &lt;code&gt;ENV&lt;/code&gt;, &lt;code&gt;ENTRYPOINT&lt;/code&gt;, &lt;code&gt;CMD&lt;/code&gt;, &lt;code&gt;HEALTHCHECK&lt;/code&gt;, &lt;code&gt;VOLUME&lt;/code&gt;, and &lt;code&gt;EXPOSE&lt;/code&gt; in your vocabulary, you have all the tools to write Dockerfiles that aren't just functional — they're configurable, predictable, and observable.&lt;/p&gt;

&lt;p&gt;Next up is BuildKit: how to enable it, what real advantages it brings over the classic builder, and how to use it to handle build secrets and package caches without compromising security.&lt;/p&gt;

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




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Open a Dockerfile from any project you have or from any open source repo. Check whether it uses shell form or exec form for &lt;code&gt;ENTRYPOINT&lt;/code&gt; and &lt;code&gt;CMD&lt;/code&gt;, whether it has a &lt;code&gt;HEALTHCHECK&lt;/code&gt;, and whether environment variables are in &lt;code&gt;ARG&lt;/code&gt; or &lt;code&gt;ENV&lt;/code&gt;. Is there anything you'd change now?&lt;/p&gt;

</description>
      <category>docker</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
  </channel>
</rss>
