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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

&lt;/div&gt;



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

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

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

&lt;p&gt;SQL exists to ask questions of your data declaratively:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;SUM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;total_spent&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'paid'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;user_id&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;total_spent&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

&lt;/div&gt;



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

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

&lt;p&gt;A relational database lets you express those rules as part of the model itself:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;orders&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;user_id&lt;/span&gt; &lt;span class="nb"&gt;INTEGER&lt;/span&gt; &lt;span class="k"&gt;REFERENCES&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

&lt;p&gt;Don't stress about memorizing this now. The goal is to give you a map, not send you out explaining distributed databases on a napkin like you're in a backend interview at 9 AM.&lt;/p&gt;

&lt;p&gt;The important idea: each type of database exists to solve different problems. But if you're learning from zero, starting with the relational model is the best investment. Many ideas about data, performance, querying, and integrity are easier to understand from there.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why PostgreSQL
&lt;/h2&gt;

&lt;p&gt;We'll use &lt;strong&gt;PostgreSQL 18&lt;/strong&gt; throughout the course.&lt;/p&gt;

&lt;p&gt;Not because it's the only option. MySQL, MariaDB, SQL Server, Oracle, and SQLite exist and have real use cases. But PostgreSQL is an excellent database to learn with and to work with seriously because it combines several things that are hard to find together:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It's &lt;strong&gt;open source&lt;/strong&gt; and widely used in the industry.&lt;/li&gt;
&lt;li&gt;It implements SQL in a powerful and mostly standard way.&lt;/li&gt;
&lt;li&gt;It has advanced types: JSONB, arrays, ranges, UUIDs, full-text search.&lt;/li&gt;
&lt;li&gt;It supports constraints, transactions, views, materialized views, roles, extensions, and serious tooling.&lt;/li&gt;
&lt;li&gt;It scales from small projects to large systems.&lt;/li&gt;
&lt;li&gt;It lets you learn general SQL while also using modern features that appear in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;PostgreSQL is a good school because it doesn't treat you like you only want to store four rows. It gives you grown-up database tools from the beginning. Some of them won't show up until much later, but it's good to know they're there, patiently waiting for you to stop writing &lt;code&gt;SELECT *&lt;/code&gt; out of habit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What learning SQL really means
&lt;/h2&gt;

&lt;p&gt;Learning SQL is not memorizing commands.&lt;/p&gt;

&lt;p&gt;Memorizing commands creates that dangerous feeling of “I know SQL” until a query appears with &lt;code&gt;LEFT JOIN&lt;/code&gt;, &lt;code&gt;GROUP BY&lt;/code&gt;, &lt;code&gt;HAVING&lt;/code&gt;, a subquery, and a &lt;code&gt;NULL&lt;/code&gt; staring from the corner. Then the castle starts wobbling.&lt;/p&gt;

&lt;p&gt;Really learning SQL means understanding questions like these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What shape does my data have?&lt;/li&gt;
&lt;li&gt;What relationship exists between these tables?&lt;/li&gt;
&lt;li&gt;Which rows am I filtering before grouping?&lt;/li&gt;
&lt;li&gt;Why is this query reading ten million rows?&lt;/li&gt;
&lt;li&gt;What index does this access pattern need?&lt;/li&gt;
&lt;li&gt;What happens if two users modify the same thing at the same time?&lt;/li&gt;
&lt;li&gt;What is my ORM actually generating?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's why this course starts here, before installing anything. Because if you understand the problem, the tool stops feeling like magic. And when the tool stops feeling like magic, you start using it with judgment.&lt;/p&gt;

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

&lt;p&gt;If you understood this, you now have the mental foundation for the course:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A file can store data, but it doesn't manage querying, relationships, concurrency, and integrity well.&lt;/li&gt;
&lt;li&gt;A database adds structure, rules, guarantees, and query capabilities.&lt;/li&gt;
&lt;li&gt;SQL lets you ask for the result you want, not manually describe how to walk through the data step by step.&lt;/li&gt;
&lt;li&gt;Relational databases remain fundamental because they model structured data and relationships very well.&lt;/li&gt;
&lt;li&gt;PostgreSQL will be our main tool because it's modern, open, powerful, and widely used in production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the next lesson we'll set up the environment: we'll run PostgreSQL 18 with Docker, connect with &lt;code&gt;psql&lt;/code&gt; and pgAdmin, and leave a database ready for real data. If Docker still looks at you funny, &lt;a href="https://dev.to/en/courses/mastering-docker-from-scratch"&gt;Mastering Docker from Scratch&lt;/a&gt; is the perfect warm-up; we'll use it here without turning this lesson into another container-moving day.&lt;/p&gt;

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

</description>
      <category>sql</category>
      <category>database</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Docker Compose networking: networks, services, and isolation</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Sat, 24 Oct 2026 07:46:27 +0000</pubDate>
      <link>https://dev.to/fj_palacios/docker-compose-networking-networks-services-and-isolation-5759</link>
      <guid>https://dev.to/fj_palacios/docker-compose-networking-networks-services-and-isolation-5759</guid>
      <description>&lt;p&gt;There is a very Docker Compose moment: Postgres is running, the API is running, the logs look reasonable, and the API still says it can't connect to &lt;code&gt;localhost:5432&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You check &lt;code&gt;docker compose ps&lt;/code&gt;. Postgres is &lt;code&gt;Up&lt;/code&gt;. You check the &lt;code&gt;docker-compose.yml&lt;/code&gt;. The port is there. You stare at the screen with that specific mix of suspicion and tiredness that only appears when a tool is technically telling the truth in the least helpful way possible.&lt;/p&gt;

&lt;p&gt;Is Postgres down? No. Is the port wrong? Maybe, but probably not. Is &lt;code&gt;localhost&lt;/code&gt; a trap with good branding? Exactly.&lt;/p&gt;

&lt;p&gt;In Docker Compose, each container lives in its own little network world. Once you understand that, the rest stops feeling like dark magic with YAML.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compose's default network
&lt;/h2&gt;

&lt;p&gt;When you run &lt;code&gt;docker compose up&lt;/code&gt;, Compose automatically creates a network for the project. If your directory is called &lt;code&gt;myapp&lt;/code&gt;, that network is usually named something like &lt;code&gt;myapp_default&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;All services in the &lt;code&gt;docker-compose.yml&lt;/code&gt; join that network by default.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp/api&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even though you didn't write &lt;code&gt;networks:&lt;/code&gt; anywhere, Compose did some work for you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
docker network &lt;span class="nb"&gt;ls&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see a network like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NETWORK ID     NAME            DRIVER    SCOPE
abc123def456   myapp_default   bridge    local
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside that network, services can find each other by &lt;strong&gt;service name&lt;/strong&gt;. The API can connect to Postgres using &lt;code&gt;db&lt;/code&gt; as the hostname.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;postgresql://myapp:secret@db:5432/myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't use the container IP. Container IPs can change when services are recreated. Using container IPs in Compose is like writing down where you parked three weeks ago on a napkin: technically useful for a few minutes.&lt;/p&gt;

&lt;p&gt;Use service names.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;localhost&lt;/code&gt; inside a container is not your machine
&lt;/h2&gt;

&lt;p&gt;This is the main trap.&lt;/p&gt;

&lt;p&gt;From your machine, &lt;code&gt;localhost&lt;/code&gt; means your machine.&lt;/p&gt;

&lt;p&gt;From inside a container, &lt;code&gt;localhost&lt;/code&gt; means that container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp/api&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://myapp:secret@localhost:5432/myapp&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is wrong for container-to-container communication. The API will look for Postgres inside the API container itself. Spoiler: it is not there. If it were, you'd have another problem, probably with a suspicious architecture smell.&lt;/p&gt;

&lt;p&gt;The correct version uses the service name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp/api&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://myapp:secret@db:5432/myapp&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mental model:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Connecting from&lt;/th&gt;
&lt;th&gt;Correct host&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Your browser to the API&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;localhost&lt;/code&gt; if the port is published&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API to Postgres&lt;/td&gt;
&lt;td&gt;&lt;code&gt;db&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;API to Redis&lt;/td&gt;
&lt;td&gt;&lt;code&gt;redis&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;One container to another&lt;/td&gt;
&lt;td&gt;The service name&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Don't stress if this feels counterintuitive at first. &lt;code&gt;localhost&lt;/code&gt; has spent years looking innocent while ruining entire afternoons with a smile.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;ports&lt;/code&gt; is not how containers talk to each other
&lt;/h2&gt;

&lt;p&gt;Another classic confusion: thinking you need to publish a port so another container can connect to it.&lt;/p&gt;

&lt;p&gt;You don't.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://myapp:secret@db:5432/myapp&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;ports: "3000:3000"&lt;/code&gt; line publishes the API port to your machine. It lets you open &lt;code&gt;http://localhost:3000&lt;/code&gt; from your browser.&lt;/p&gt;

&lt;p&gt;But the API does not need Postgres to have &lt;code&gt;ports: "5432:5432"&lt;/code&gt; to connect to &lt;code&gt;db:5432&lt;/code&gt;. Both services are already on the internal Compose network.&lt;/p&gt;

&lt;p&gt;This matters for security. If you publish Postgres like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;5432:5432"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You're exposing the database to the host. In development that can be useful if you want to connect with TablePlus, DBeaver, local &lt;code&gt;psql&lt;/code&gt;, or another external tool. But don't do it “just in case”. “Just in case” in infrastructure is a polite way of saying “I'll leave this door open and see what happens”.&lt;/p&gt;

&lt;p&gt;If only the API needs to talk to the database, don't publish the port.&lt;/p&gt;

&lt;h2&gt;
  
  
  Custom networks: separate rooms
&lt;/h2&gt;

&lt;p&gt;The default network is fine for small projects. All services can see each other and life goes on.&lt;/p&gt;

&lt;p&gt;But in a more realistic application, you might have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;web&lt;/code&gt;: frontend or reverse proxy.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;api&lt;/code&gt;: backend.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;db&lt;/code&gt;: database.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;redis&lt;/code&gt;: cache.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is no reason for &lt;code&gt;web&lt;/code&gt; to connect directly to &lt;code&gt;db&lt;/code&gt;. The frontend talks to the API. The API talks to the database. The database should not be standing in the lobby waving at the entire building.&lt;/p&gt;

&lt;p&gt;You can model that with custom networks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;frontend&lt;/span&gt;

  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp/api&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;frontend&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the rules are clear:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Networks&lt;/th&gt;
&lt;th&gt;Can talk to&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;web&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;frontend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;api&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;frontend&lt;/code&gt;, &lt;code&gt;backend&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;web&lt;/code&gt;, &lt;code&gt;db&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;db&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;backend&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;api&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;web&lt;/code&gt; and &lt;code&gt;db&lt;/code&gt; do not share a network, so they cannot resolve each other by name or connect directly. This does not replace authentication, firewalls, permissions, or common sense, but it reduces surface area. And reducing surface area is one of those boring things that prevents interesting incidents. Interesting incidents are overrated.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;code&gt;expose&lt;/code&gt; vs &lt;code&gt;ports&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Compose also has &lt;code&gt;expose&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp/api&lt;/span&gt;
    &lt;span class="na"&gt;expose&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;expose&lt;/code&gt; documents that a service listens on a port for other services on the network, but it does not publish that port to your machine.&lt;/p&gt;

&lt;p&gt;In practice, services on the same network can connect to a container port even if you don't write &lt;code&gt;expose&lt;/code&gt;. That's why many teams don't use it. It can be useful as documentation inside the &lt;code&gt;docker-compose.yml&lt;/code&gt;, but don't confuse it with security.&lt;/p&gt;

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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Publishes to host&lt;/th&gt;
&lt;th&gt;Main use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ports&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Yes&lt;/td&gt;
&lt;td&gt;Access from your machine or outside&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;expose&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;No&lt;/td&gt;
&lt;td&gt;Document internal ports&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you want to open something from the browser, use &lt;code&gt;ports&lt;/code&gt;. If only containers talk to each other, you usually don't need anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  External networks
&lt;/h2&gt;

&lt;p&gt;Sometimes you want several Compose projects to share a network. For example, a common reverse proxy (&lt;code&gt;traefik&lt;/code&gt;, &lt;code&gt;nginx&lt;/code&gt;, &lt;code&gt;caddy&lt;/code&gt;) routing multiple local apps or services on the same server.&lt;/p&gt;

&lt;p&gt;First create the network manually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create shared-proxy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then declare it as external:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp/api&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;shared-proxy&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;shared-proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;external&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;external: true&lt;/code&gt; means: “Compose, don't create this network; it already exists, use it.”&lt;/p&gt;

&lt;p&gt;If the network doesn't exist, Compose fails. And that's good. Better to fail than silently create a new network and leave you wondering for half an hour why the proxy can't see your app. Docker already has enough material to confuse you without extra help.&lt;/p&gt;

&lt;h2&gt;
  
  
  Network aliases
&lt;/h2&gt;

&lt;p&gt;By default, each service resolves by its name: &lt;code&gt;api&lt;/code&gt;, &lt;code&gt;db&lt;/code&gt;, &lt;code&gt;redis&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Sometimes you want additional names. That's what &lt;strong&gt;aliases&lt;/strong&gt; are for.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp/api&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;aliases&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api-service&lt;/span&gt;
          &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;internal-api&lt;/span&gt;

  &lt;span class="na"&gt;worker&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp/worker&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;From &lt;code&gt;worker&lt;/code&gt;, these hostnames point to the &lt;code&gt;api&lt;/code&gt; service:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;api
api-service
internal-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use this carefully. An alias can help when a legacy application expects a specific hostname. But if you start adding three names per service “for convenience”, you'll end up with a tiny home-made DNS system inside YAML. Like having five nicknames for the same person in a group chat: funny until someone new tries to understand the conversation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Complete example
&lt;/h2&gt;

&lt;p&gt;A reasonable stack with reverse proxy, API, database, and cache:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;web&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;nginx:alpine&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;8080:80"&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;frontend&lt;/span&gt;

  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://myapp:secret@db:5432/myapp&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://redis:6379&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;frontend&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;

  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db-data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;myapp"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;frontend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice three details:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Only &lt;code&gt;web&lt;/code&gt; publishes a port to the host.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;api&lt;/code&gt; talks to &lt;code&gt;db&lt;/code&gt; and &lt;code&gt;redis&lt;/code&gt; using service names.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;db&lt;/code&gt; and &lt;code&gt;redis&lt;/code&gt; are not on &lt;code&gt;frontend&lt;/code&gt;, so &lt;code&gt;web&lt;/code&gt; cannot talk to them directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This does not turn your app into Fort Knox. But you're no longer putting every service in the same room with a sticky note that says “be nice”.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical challenge
&lt;/h2&gt;

&lt;p&gt;Take the &lt;code&gt;docker-compose.yml&lt;/code&gt; from the previous tutorial and split it into two networks: &lt;code&gt;frontend&lt;/code&gt; and &lt;code&gt;backend&lt;/code&gt;. The API should be on both. Postgres and Redis should only be on &lt;code&gt;backend&lt;/code&gt;. If you have a web or proxy service, put it only on &lt;code&gt;frontend&lt;/code&gt; and publish only its port.&lt;/p&gt;

&lt;p&gt;Then verify that your internal URLs use service names (&lt;code&gt;db&lt;/code&gt;, &lt;code&gt;redis&lt;/code&gt;, &lt;code&gt;api&lt;/code&gt;) and not &lt;code&gt;localhost&lt;/code&gt;. If you find a &lt;code&gt;localhost&lt;/code&gt; inside an environment variable used between containers, don't look at it fondly: change it.&lt;/p&gt;




&lt;p&gt;You now have the right mental map: Compose creates a default network, services discover each other by name, &lt;code&gt;ports&lt;/code&gt; is for reaching your machine, and custom networks let you isolate what should not talk directly. In the next tutorial we'll close the Compose module with best practices: environment-specific files, secrets, resource limits, logging, and the small details that separate a useful &lt;code&gt;docker-compose.yml&lt;/code&gt; from one that looks written during a power outage.&lt;/p&gt;

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

</description>
      <category>docker</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Docker Compose volumes and data persistence</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Fri, 23 Oct 2026 05:38:35 +0000</pubDate>
      <link>https://dev.to/fj_palacios/docker-compose-volumes-and-data-persistence-419j</link>
      <guid>https://dev.to/fj_palacios/docker-compose-volumes-and-data-persistence-419j</guid>
      <description>&lt;p&gt;Docker has a very effective teaching method: you start Postgres, insert data, stop the container, start it again, and everything is still there. Nice. You gain confidence. You clean things up. You run the wrong command. You open the app again and the database is emptier than a sprint retrospective where everyone says “all good”.&lt;/p&gt;

&lt;p&gt;Where did the data live? Inside the container? Somewhere in Docker? In a secret folder guarded by a small daemon with trust issues?&lt;/p&gt;

&lt;p&gt;Take a breath. Docker doesn't hate your data. Containers are disposable by design. If you want persistence, you need to declare it explicitly. That's what &lt;strong&gt;volumes&lt;/strong&gt; are for.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: containers are not hard drives
&lt;/h2&gt;

&lt;p&gt;A container has its own filesystem. You can write files, modify data, create directories, and feel like everything is under control. The catch is that this writable layer belongs to the container.&lt;/p&gt;

&lt;p&gt;Remove the container, and those changes go with it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--name&lt;/span&gt; temp-postgres &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret &lt;span class="nt"&gt;-d&lt;/span&gt; postgres:16-alpine
docker &lt;span class="nb"&gt;rm&lt;/span&gt; &lt;span class="nt"&gt;-f&lt;/span&gt; temp-postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container is gone. Its writable changes are gone too.&lt;/p&gt;

&lt;p&gt;That's not a bug. That's the model. Containers should be safe to destroy and recreate. The image contains the application; the data lives outside.&lt;/p&gt;

&lt;p&gt;Think of a container as a moving van. Useful, necessary, maybe smelling vaguely of cheap air freshener. But you don't store your lease, family photos, and the only copy of your production database inside the van. Those go in labeled boxes.&lt;/p&gt;

&lt;p&gt;In Docker, those boxes are volumes.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three volume types you'll actually use
&lt;/h2&gt;

&lt;p&gt;In Compose, a service can mount several kinds of storage:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app-data:/app/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./src:/app/src&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/app/temp&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config:/app/config:ro&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those four lines are not the same thing. Treating them as interchangeable is how you end up staring at &lt;code&gt;docker volume ls&lt;/code&gt; like you're reading ancient ruins.&lt;/p&gt;

&lt;h3&gt;
  
  
  Named volumes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;app-data:/app/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;named volume&lt;/strong&gt; is storage managed by Docker. You give it a name (&lt;code&gt;app-data&lt;/code&gt;), and Docker decides where it physically lives on the host.&lt;/p&gt;

&lt;p&gt;This is the default choice for data you want to keep: databases, uploaded files, persistent caches, internal application state.&lt;/p&gt;

&lt;p&gt;The upside: it works consistently across Linux, macOS, and Windows. You don't depend on a specific host path.&lt;/p&gt;

&lt;p&gt;The downside: the files are not sitting neatly inside your project directory. Docker stores them internally. If you're like me when I started, that creates a very specific anxiety: “I know my data exists, but I don't know where it lives.” Correct. Welcome to the club.&lt;/p&gt;

&lt;p&gt;You can inspect volumes with:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Compose usually prefixes volume names with the project name. If your directory is called &lt;code&gt;myproject&lt;/code&gt; and your volume is &lt;code&gt;app-data&lt;/code&gt;, the real volume might be &lt;code&gt;myproject_app-data&lt;/code&gt;. Docker isn't trying to be mysterious. It just has a gift for making simple things look slightly like infrastructure archaeology.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bind mounts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./src:/app/src&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;strong&gt;bind mount&lt;/strong&gt; mounts a host directory into the container. Here, &lt;code&gt;./src&lt;/code&gt; on your machine appears as &lt;code&gt;/app/src&lt;/code&gt; inside the container.&lt;/p&gt;

&lt;p&gt;This is excellent for development: edit code in your editor, the container sees the change immediately, the dev server reloads, and for one beautiful second software feels like it was designed by someone on your side.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./api/src:/app/src&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But bind mounts depend on the host filesystem. If &lt;code&gt;./api/src&lt;/code&gt; exists on your laptop but not on the server, deployment fails. If file synchronization is slow on macOS, you'll feel it. If someone changes the path, the container does not develop intuition.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Development&lt;/strong&gt;: bind mounts for source code and local configuration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Persistent data&lt;/strong&gt;: named volumes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production&lt;/strong&gt;: avoid mounting source code from the host unless you have a VERY clear reason.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Not “interesting”. Clear.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anonymous volumes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/app/temp&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An &lt;strong&gt;anonymous volume&lt;/strong&gt; has no explicit name. Docker creates one automatically for &lt;code&gt;/app/temp&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This can be useful for internal container paths, but for important data it's a trap with good lighting. The volume exists, yes, but you didn't choose a stable name for it. Weeks later, you'll see a list of long IDs and have to guess which one contains what.&lt;/p&gt;

&lt;p&gt;It's like having a drawer full of unlabelled cables. One of them is definitely important. Good luck finding the right charger before USB invents another connector.&lt;/p&gt;

&lt;p&gt;For data you want to keep and understand, use named volumes.&lt;/p&gt;

&lt;h3&gt;
  
  
  Read-only volumes
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;app&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./config:/app/config:ro&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;:ro&lt;/code&gt; suffix mounts the volume as &lt;strong&gt;read-only&lt;/strong&gt;. The container can read &lt;code&gt;/app/config&lt;/code&gt;, but it can't write there.&lt;/p&gt;

&lt;p&gt;Use this for configuration, certificates, static files, or anything the container should not modify. If an application only needs to read a file, don't give it write access. Least privilege applies locally too. Your laptop is not a moral exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  Database persistence
&lt;/h2&gt;

&lt;p&gt;The most common Compose case is a database.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db-data:/var/lib/postgresql/data&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Postgres stores its data in &lt;code&gt;/var/lib/postgresql/data&lt;/code&gt;. By mounting &lt;code&gt;db-data&lt;/code&gt; there, the data lives in the volume, not in the container's writable layer.&lt;/p&gt;

&lt;p&gt;Now you can remove and recreate the container without losing the database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The container changes. The volume remains.&lt;/p&gt;

&lt;p&gt;But pay attention: &lt;code&gt;down&lt;/code&gt; and &lt;code&gt;down -v&lt;/code&gt; are not polite cousins. &lt;code&gt;docker compose down&lt;/code&gt; removes containers and networks, but keeps named volumes. &lt;code&gt;docker compose down -v&lt;/code&gt; also removes the volumes declared by Compose.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down     &lt;span class="c"&gt;# Keeps named volumes&lt;/span&gt;
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;  &lt;span class="c"&gt;# Removes named volumes too&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That &lt;code&gt;-v&lt;/code&gt; is tiny, quiet, and destructive. Like a red button without a safety cover. Use it when you deliberately want to delete data: resetting a local database, cleaning a test environment, starting from scratch.&lt;/p&gt;

&lt;p&gt;Don't use it as “general cleanup” if the data matters.&lt;/p&gt;

&lt;p&gt;For other databases, the idea is the same: mount the volume where the image stores its data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;postgres-data:/var/lib/postgresql/data&lt;/span&gt;

  &lt;span class="na"&gt;mongo&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mongo:7&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mongo-data:/data/db&lt;/span&gt;

  &lt;span class="na"&gt;mysql&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;mysql:8&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;MYSQL_ROOT_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;mysql-data:/var/lib/mysql&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;postgres-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mongo-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;mysql-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The path changes depending on the image. The pattern doesn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initialization scripts
&lt;/h2&gt;

&lt;p&gt;Many official database images can run initialization scripts the first time the database is created.&lt;/p&gt;

&lt;p&gt;With Postgres, anything mounted into &lt;code&gt;/docker-entrypoint-initdb.d&lt;/code&gt; runs when the data directory is initialized:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db-data:/var/lib/postgresql/data&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./init-scripts:/docker-entrypoint-initdb.d:ro&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- init-scripts/01-create-tables.sql&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt; &lt;span class="n"&gt;IF&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;EXISTS&lt;/span&gt; &lt;span class="n"&gt;notes&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="nb"&gt;SERIAL&lt;/span&gt; &lt;span class="k"&gt;PRIMARY&lt;/span&gt; &lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;TEXT&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="nb"&gt;TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;NOT&lt;/span&gt; &lt;span class="k"&gt;NULL&lt;/span&gt; &lt;span class="k"&gt;DEFAULT&lt;/span&gt; &lt;span class="n"&gt;NOW&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Important detail: these scripts run when the data directory is empty. If the volume already exists and Postgres has already initialized, they do not run again.&lt;/p&gt;

&lt;p&gt;Feeling the “but I changed the SQL and nothing happened” moment? Good. The volume already had data. Docker is not going to wipe your database because you edited a file. That would be helpful in a deeply criminal way.&lt;/p&gt;

&lt;p&gt;To test initialization changes locally:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That deletes the volume and forces a fresh initialization. Deliberate, visible, consequences included.&lt;/p&gt;

&lt;h2&gt;
  
  
  Development vs production
&lt;/h2&gt;

&lt;p&gt;Volumes also separate two very different needs: fast development and stable runtime behavior.&lt;/p&gt;

&lt;p&gt;In development, you want hot reload:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.dev.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;development&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./api/src:/app/src&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./api/package.json:/app/package.json:ro&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/app/node_modules&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm run dev&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;- /app/node_modules&lt;/code&gt; line creates an anonymous volume so the container's &lt;code&gt;node_modules&lt;/code&gt; directory doesn't get overwritten by the host. This trick exists because JavaScript decided dependencies should have more emotional mass than the actual project. We're not fixing that today.&lt;/p&gt;

&lt;p&gt;In production, you don't mount source code from your machine. You build an image and only mount the data that must persist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.prod.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;
      &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api-data:/app/data&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm start&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then combine files depending on the environment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Development&lt;/span&gt;
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.yml &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.dev.yml up

&lt;span class="c"&gt;# Production&lt;/span&gt;
docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.yml &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.prod.yml up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The base &lt;code&gt;docker-compose.yml&lt;/code&gt; describes the shared stack, and the environment-specific files override what changes. Just like with &lt;a href="https://dev.to/en/courses/mastering-git-from-scratch"&gt;Git&lt;/a&gt;, you commit what describes the project and keep machine-specific details out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Backups: persistence is not a backup
&lt;/h2&gt;

&lt;p&gt;A persistent volume is not a backup. Say it with me: &lt;strong&gt;persistence is not backup&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A named volume survives container recreation. It does not protect you from &lt;code&gt;docker compose down -v&lt;/code&gt;, manual deletion, data corruption, an overconfident script, or you on a Friday afternoon thinking “this definitely won't break anything”.&lt;/p&gt;

&lt;p&gt;For databases, the best backup is usually the database's own tool. For Postgres:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;db pg_dump &lt;span class="nt"&gt;-U&lt;/span&gt; myapp myapp &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; backup.sql
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That runs &lt;code&gt;pg_dump&lt;/code&gt; inside the &lt;code&gt;db&lt;/code&gt; container and writes the output to your machine. Clear, portable, and database-aware.&lt;/p&gt;

&lt;p&gt;You can also make a low-level copy of a volume using a temporary container:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; myproject_db-data:/data:ro &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/backup &lt;span class="se"&gt;\&lt;/span&gt;
  alpine &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tar &lt;/span&gt;czf /backup/db-data-backup.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /data &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What's happening here:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;myproject_db-data:/data:ro&lt;/code&gt; mounts the volume read-only.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"$(pwd)":/backup&lt;/code&gt; mounts your current directory as the backup destination.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;tar czf&lt;/code&gt; archives the volume contents.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;--rm&lt;/code&gt; removes the temporary container when it exits.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've spent years fighting shell quotes, you noticed &lt;code&gt;"$(pwd)"&lt;/code&gt;. If not, don't worry: Bash will get its own course later, because this topic deserves more than “copy this and trust me”. That's not decoration. It keeps the command from breaking when the path contains spaces. The shell is always waiting for a chance to remind you that quotes are not optional.&lt;/p&gt;

&lt;p&gt;To restore into an empty volume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume create myproject_db-data

docker run &lt;span class="nt"&gt;--rm&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; myproject_db-data:/data &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;pwd&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;:/backup:ro &lt;span class="se"&gt;\&lt;/span&gt;
  alpine &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nb"&gt;tar &lt;/span&gt;xzf /backup/db-data-backup.tar.gz &lt;span class="nt"&gt;-C&lt;/span&gt; /data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For databases that are actively running, don't blindly copy files while the engine is writing. It might help in development. In production, use &lt;code&gt;pg_dump&lt;/code&gt;, consistent snapshots, or the official tooling for your database. Faith can move mountains, but it does not guarantee transactional integrity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup without destroying what matters
&lt;/h2&gt;

&lt;p&gt;These Compose and Docker commands have very different effects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose stop
docker compose down
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;
docker volume &lt;span class="nb"&gt;ls
&lt;/span&gt;docker volume &lt;span class="nb"&gt;rm &lt;/span&gt;myproject_db-data
docker volume prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker compose stop&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stops containers, removes nothing&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker compose down&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes containers and networks, keeps named volumes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker compose down -v&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes containers, networks, and project volumes&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker volume rm &amp;lt;name&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes one specific volume&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;docker volume prune&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Removes volumes not used by any container&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;docker volume prune&lt;/code&gt; asks for confirmation, but it does not know which volume was “emotionally important”. It only knows whether a container is currently using it. If you have an old volume with data you care about and no container is attached to it right now, it can be removed.&lt;/p&gt;

&lt;p&gt;Before deleting, inspect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker volume inspect myproject_db-data
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And if you're unsure, back it up first. Boring advice. So is wearing a seatbelt, until the day it stops being boring.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical challenge
&lt;/h2&gt;

&lt;p&gt;Create a &lt;code&gt;docker-compose.yml&lt;/code&gt; with Postgres and a fake API or any lightweight image you know. Add a named volume for Postgres, start the stack, create some data, run &lt;code&gt;docker compose down&lt;/code&gt;, then start it again and confirm the data is still there.&lt;/p&gt;

&lt;p&gt;After that, back up the volume using the temporary &lt;code&gt;alpine&lt;/code&gt; container. You don't need an enterprise backup system yet; just understand the flow: volume → &lt;code&gt;.tar.gz&lt;/code&gt; archive → restore into an empty volume.&lt;/p&gt;




&lt;p&gt;You can now treat Docker Compose data with the respect it deserves: outside the container, with clear names, with clean separation between development and production, and with backups that don't depend on crossed fingers. In the next tutorial we'll go into Docker Compose networking: default networks, custom networks, service isolation, and why your database should not be cheerfully waving at the entire stack.&lt;/p&gt;

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

</description>
      <category>docker</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Setting up your DevOps work environment: Linux, terminal, and essential tools</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Thu, 22 Oct 2026 06:29:04 +0000</pubDate>
      <link>https://dev.to/fj_palacios/setting-up-your-devops-work-environment-linux-terminal-and-essential-tools-ap2</link>
      <guid>https://dev.to/fj_palacios/setting-up-your-devops-work-environment-linux-terminal-and-essential-tools-ap2</guid>
      <description>&lt;p&gt;The first real collision with DevOps usually isn't Kubernetes. Or Terraform. Or that YAML file that looks like it was written by someone with strong feelings against indentation.&lt;/p&gt;

&lt;p&gt;The first collision is usually less heroic: you try to prepare your environment, open a terminal, see three different ways to “use Linux,” someone mentions SSH keys, someone else says “just use WSL,” another person says “spin up a cloud instance,” and suddenly installing a few tools feels like choosing a mortgage.&lt;/p&gt;

&lt;p&gt;Should you use local Linux? A VM? WSL? AWS? GCP? Will your laptop explode? No. Unless you run too many browser tabs while VirtualBox is trying to breathe. But conceptually, no.&lt;/p&gt;

&lt;p&gt;This lesson sets up a serious working environment for the course: real enough to learn DevOps properly, simple enough that you don't spend three days configuring the workshop before touching a single tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a DevOps learning environment actually needs
&lt;/h2&gt;

&lt;p&gt;A DevOps environment does not need to look like production from minute one. That would be like learning to drive by entering a six-lane roundabout with a truck behind you. Educational, sure. Also unnecessarily traumatic.&lt;/p&gt;

&lt;p&gt;What it does need is a place where you can practice four things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;using a &lt;strong&gt;Linux terminal&lt;/strong&gt; comfortably;&lt;/li&gt;
&lt;li&gt;installing command-line tools;&lt;/li&gt;
&lt;li&gt;running services, scripts, and processes;&lt;/li&gt;
&lt;li&gt;connecting to remote machines through &lt;strong&gt;SSH&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters. DevOps lives a lot on servers. Even when we later work with containers, Kubernetes, or cloud providers, the base is still the same: a shell, permissions, processes, logs, networking, and files.&lt;/p&gt;

&lt;p&gt;If you don't have a strong terminal foundation yet, that's fine. This course starts exactly there. And if Git still feels like a black box full of branches, &lt;a href="https://dev.to/en/courses/mastering-git-from-scratch"&gt;Mastering Git from Scratch&lt;/a&gt; will save you several uncomfortable conversations with yourself.&lt;/p&gt;

&lt;p&gt;First the judgment; then the options.&lt;/p&gt;

&lt;h2&gt;
  
  
  The recommended option: a local Ubuntu VM
&lt;/h2&gt;

&lt;p&gt;To follow this course consistently, the recommended option is a &lt;strong&gt;local VM running Ubuntu 24.04 LTS&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Not because Ubuntu is magically better, but because it gives us a shared baseline: real Linux, &lt;code&gt;systemd&lt;/code&gt;, &lt;code&gt;systemctl&lt;/code&gt;, &lt;code&gt;journalctl&lt;/code&gt;, networking, permissions, packages, and a machine you can break without touching your main system. Think of it as a small practice server at home. If you destroy it, delete it and create another one. Much better than learning electricity by sticking a screwdriver into the main panel.&lt;/p&gt;

&lt;p&gt;We'll use &lt;strong&gt;Multipass&lt;/strong&gt;, a Canonical tool for creating Ubuntu VMs from the CLI. The picture looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;your machine ── multipass ──&amp;gt; Ubuntu VM devops-lab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before continuing: Multipass is recommended because it lets us create an Ubuntu VM in a simple and reproducible way. But it is not a religious requirement.&lt;/p&gt;

&lt;p&gt;If you work on a corporate machine, have security restrictions, already use a virtualization tool approved by your company, or your computer needs a special setup, use whatever method fits your environment: VirtualBox, VMware, UTM, Hyper-V, Parallels, libvirt, or anything else you are allowed to use.&lt;/p&gt;

&lt;p&gt;The tool is not the goal. The goal is to end up with an Ubuntu 24.04 LTS VM —or an equivalent Linux distribution— that you can access, where you can install packages, run commands, manage services, and practice without touching your main system.&lt;/p&gt;

&lt;p&gt;DevOps is about understanding systems, not marrying the first installer mentioned in a tutorial.&lt;/p&gt;

&lt;h2&gt;
  
  
  Installing Multipass
&lt;/h2&gt;

&lt;p&gt;Install Multipass according to your system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS and Linux (Homebrew)&lt;/span&gt;
brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; multipass

&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;snap &lt;span class="nb"&gt;install &lt;/span&gt;multipass

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

&lt;/div&gt;



&lt;p&gt;Arch users already know the drill: if &lt;code&gt;multipassd&lt;/code&gt; won't start, install &lt;code&gt;dnsmasq&lt;/code&gt; and add &lt;code&gt;ReadWritePaths=/usr/local/etc/multipassd&lt;/code&gt; to the service. If QEMU can't find &lt;code&gt;OVMF.fd&lt;/code&gt;, symlink it from &lt;code&gt;edk2-ovmf&lt;/code&gt; to &lt;code&gt;/usr/share/qemu/&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;On Windows, install it from PowerShell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;winget&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;-e&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;Canonical.Multipass&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If virtualization is disabled in your BIOS/UEFI, your company policies are strict, or Hyper-V is doing Hyper-V things, Multipass may complain. Don't stress: DevOps does not hate you personally. It's virtualization. Sometimes it feels like opening a door; sometimes it feels like getting approval from five departments to move a chair.&lt;/p&gt;

&lt;p&gt;Once installed, check that it responds:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Now we can create the VM.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the course VM
&lt;/h2&gt;

&lt;p&gt;The course VM will be called &lt;code&gt;devops-lab&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass launch 24.04 &lt;span class="nt"&gt;--name&lt;/span&gt; devops-lab &lt;span class="nt"&gt;--cpus&lt;/span&gt; 2 &lt;span class="nt"&gt;--memory&lt;/span&gt; 4G &lt;span class="nt"&gt;--disk&lt;/span&gt; 30G
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command creates an Ubuntu 24.04 machine with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;2 CPUs;&lt;/li&gt;
&lt;li&gt;4 GB of RAM;&lt;/li&gt;
&lt;li&gt;30 GB of disk;&lt;/li&gt;
&lt;li&gt;the name &lt;code&gt;devops-lab&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your computer is low on resources, you can reduce it to &lt;code&gt;--memory 2G&lt;/code&gt;, but 4 GB will be more comfortable for what we'll do later. A tiny VM works, sure, like a chair with one loose leg works. The question is how much trust you want to place in it.&lt;/p&gt;

&lt;p&gt;To list your VMs:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;To enter the VM:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Multipass already knows how to enter the machine. You don't need to configure SSH manually for everyday course work. It's a convenient door managed by the tool.&lt;/p&gt;

&lt;p&gt;To exit:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;To stop the VM:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;To start it again:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;And if you break everything with educational enthusiasm:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Breaking a lab VM is not a tragedy. It's almost part of the process. Like burning the first pancake: you don't serve it, but it teaches you how hot the pan is.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accepted alternatives
&lt;/h2&gt;

&lt;p&gt;Multipass will be the baseline for examples, but it is not the only valid way to follow the course.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Option&lt;/th&gt;
&lt;th&gt;When to use it&lt;/th&gt;
&lt;th&gt;Main trade-off&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Multipass&lt;/td&gt;
&lt;td&gt;Recommended course baseline&lt;/td&gt;
&lt;td&gt;Depends on local virtualization&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;WSL2&lt;/td&gt;
&lt;td&gt;You use Windows and Multipass does not fit your setup&lt;/td&gt;
&lt;td&gt;Very practical, with a few Windows moments&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Local Linux&lt;/td&gt;
&lt;td&gt;You already use Linux and know what you're touching&lt;/td&gt;
&lt;td&gt;You can break your real system&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;td&gt;You want real public infrastructure&lt;/td&gt;
&lt;td&gt;More realistic, but billing enters chat&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  WSL2 on Windows
&lt;/h3&gt;

&lt;p&gt;If you work on Windows and Multipass gives you trouble, &lt;strong&gt;WSL2&lt;/strong&gt; is a reasonable alternative. Microsoft documents the basic installation as a single command: &lt;code&gt;wsl --install&lt;/code&gt;, which installs WSL and, by default, Ubuntu.&lt;/p&gt;

&lt;p&gt;From PowerShell as administrator:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight powershell"&gt;&lt;code&gt;&lt;span class="n"&gt;wsl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nt"&gt;--install&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Restart if Windows asks for it. Windows asking for a restart: the ritual that keeps the ecosystem humble.&lt;/p&gt;

&lt;p&gt;WSL2 works for many parts of the course, but it is not exactly a traditional VM. Filesystem, networking, and services can behave differently. Great to start; not always identical to a server.&lt;/p&gt;

&lt;h3&gt;
  
  
  Local Linux
&lt;/h3&gt;

&lt;p&gt;If you already use Linux, perfect. You're home. You probably also have an opinion about your distro, terminal, and package manager. Arch users already know the drill.&lt;/p&gt;

&lt;p&gt;You can follow the course on your real system, but at the beginning it helps to separate learning from your daily machine. You don't learn carpentry by removing your front door first.&lt;/p&gt;

&lt;h3&gt;
  
  
  Cloud later
&lt;/h3&gt;

&lt;p&gt;Cloud will show up later when we actually need it: IAM, security groups, cloud networking, Terraform against real infrastructure, managed services, and other pieces with consequences.&lt;/p&gt;

&lt;p&gt;Can free tier work? Yes, if you respect limits and conditions. Am I going to promise that the whole course will be free on cloud? No. That would be like saying “don't worry, this YAML will definitely work the first time.” Technically possible; emotionally irresponsible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Entering with Multipass and practicing SSH
&lt;/h2&gt;

&lt;p&gt;For course work, you'll usually enter like this:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;You can also run commands without opening a full shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass &lt;span class="nb"&gt;exec &lt;/span&gt;devops-lab &lt;span class="nt"&gt;--&lt;/span&gt; lsb_release &lt;span class="nt"&gt;-a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But &lt;strong&gt;SSH&lt;/strong&gt; is still fundamental in DevOps. On real servers, cloud instances, and many lab environments, SSH will be your front door.&lt;/p&gt;

&lt;p&gt;So let's practice it against our local VM. First generate a key if you don't already have one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh-keygen &lt;span class="nt"&gt;-t&lt;/span&gt; ed25519 &lt;span class="nt"&gt;-C&lt;/span&gt; &lt;span class="s2"&gt;"devops-course"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Accept the default path (&lt;code&gt;~/.ssh/id_ed25519&lt;/code&gt;) unless you have a reason to change it. The public part (&lt;code&gt;id_ed25519.pub&lt;/code&gt;) goes to the remote machine. The private part (&lt;code&gt;id_ed25519&lt;/code&gt;) is not shared. Ever. Think house keys: you can give the lock a copy of your public identity, but you don't post your private key in a team chat. Obvious, until it happens. And it happens.&lt;/p&gt;

&lt;p&gt;Copy the public key into the VM:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;multipass transfer ~/.ssh/id_ed25519.pub devops-lab:/home/ubuntu/id_ed25519.pub
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Enter the VM with Multipass:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Inside the VM, add the key to &lt;code&gt;authorized_keys&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.ssh
&lt;span class="nb"&gt;cat&lt;/span&gt; ~/id_ed25519.pub &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.ssh/authorized_keys
&lt;span class="nb"&gt;chmod &lt;/span&gt;700 ~/.ssh
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 ~/.ssh/authorized_keys
&lt;span class="nb"&gt;rm&lt;/span&gt; ~/id_ed25519.pub
&lt;span class="nb"&gt;exit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now check the VM IP:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Look for the &lt;code&gt;IPv4&lt;/code&gt; line and connect through SSH:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh ubuntu@&amp;lt;vm-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need a specific key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ~/.ssh/id_ed25519 ubuntu@&amp;lt;vm-ip&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this does not work on your system, that's fine: some drivers, firewalls, or networking setups may block direct access by IP. We'll use &lt;code&gt;multipass shell&lt;/code&gt; to follow the course; SSH is extra practice here, and we'll revisit it with cloud later.&lt;/p&gt;

&lt;p&gt;The important idea is this: Multipass gives you the convenient door; SSH is the door you'll use on real servers. You should know both.&lt;/p&gt;

&lt;p&gt;Now that you have a stable way into a Linux machine, you need the base tools.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essential tools
&lt;/h2&gt;

&lt;p&gt;We'll install the minimum reasonable toolkit for the first lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;git&lt;/code&gt;, for versioning files and scripts;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl&lt;/code&gt; and &lt;code&gt;wget&lt;/code&gt;, for making requests and downloading resources;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;vim&lt;/code&gt;, because sooner or later you'll edit something in a terminal;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;htop&lt;/code&gt;, for reading processes without suffering through &lt;code&gt;top&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;jq&lt;/code&gt;, for working with JSON;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;net-tools&lt;/code&gt; or modern equivalents, for basic networking commands.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Vim traps you and you can't exit, it's not a bug, it's an initiation ceremony. &lt;a href="https://dev.to/en/courses/mastering-vim-from-scratch"&gt;Mastering Vim from Scratch&lt;/a&gt; starts exactly from that kind of existential moment.&lt;/p&gt;

&lt;p&gt;If you're following the recommended path, enter the VM first with &lt;code&gt;multipass shell devops-lab&lt;/code&gt; and use the Ubuntu/Debian commands. If you're using another alternative, adapt the installation to your system:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS and Linux (Homebrew)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;git curl wget vim htop jq

&lt;span class="c"&gt;# Ubuntu / Debian&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt update
&lt;span class="nb"&gt;sudo &lt;/span&gt;apt &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-y&lt;/span&gt; git curl wget vim htop jq net-tools

&lt;span class="c"&gt;# Arch Linux&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;pacman &lt;span class="nt"&gt;-S&lt;/span&gt; git curl wget vim htop jq net-tools
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then verify everything responds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git &lt;span class="nt"&gt;--version&lt;/span&gt;
curl &lt;span class="nt"&gt;--version&lt;/span&gt;
vim &lt;span class="nt"&gt;--version&lt;/span&gt;
jq &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You don't need to memorize every tool now. &lt;code&gt;git&lt;/code&gt; gives us traceability; &lt;code&gt;curl&lt;/code&gt; lets us talk to services; &lt;code&gt;jq&lt;/code&gt; lets us read JSON without crying; &lt;code&gt;htop&lt;/code&gt; shows processes with less of a 1998 accounting spreadsheet vibe.&lt;/p&gt;

&lt;p&gt;With tools ready, let's make the terminal more comfortable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Configuring a comfortable terminal
&lt;/h2&gt;

&lt;p&gt;You could do the entire course with the default shell, no plugins, and no colors. You could also write all your code in a tiny-font plain text editor. Possible is not the same as wise.&lt;/p&gt;

&lt;p&gt;For now, &lt;code&gt;bash&lt;/code&gt; is fine. In fact, it's what you'll find on most servers you connect to. But this is your work terminal, your personal environment — you can afford some luxuries here. If you want a nicer experience, install &lt;code&gt;zsh&lt;/code&gt; and, optionally, Oh My Zsh. If you want the slower walkthrough, there is already a dedicated tutorial on &lt;a href="https://dev.to/en/tutorials/how-to-install-zsh-oh-my-zsh-gnu-linux"&gt;how to install ZSH and Oh My ZSH! in GNU/Linux&lt;/a&gt;, which fits this Ubuntu VM especially well.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# macOS and Linux (Homebrew)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;zsh

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

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

&lt;/div&gt;



&lt;p&gt;Oh My Zsh is installed through a remote script. That's common, but it is still a remote script. The responsible version: read it before running it. The realistic version: at least run it from the official source and not from &lt;code&gt;random-devops-blog-2020.example&lt;/code&gt;, please.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sh &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After installing, change your default shell to &lt;code&gt;zsh&lt;/code&gt;. If you are using the recommended Multipass VM, the &lt;code&gt;ubuntu&lt;/code&gt; user has no password, so use &lt;code&gt;sudo&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;chsh &lt;span class="nt"&gt;-s&lt;/span&gt; &lt;span class="si"&gt;$(&lt;/span&gt;which zsh&lt;span class="si"&gt;)&lt;/span&gt; ubuntu
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On other systems, &lt;code&gt;chsh&lt;/code&gt; may ask for your password without &lt;code&gt;sudo&lt;/code&gt;. Both achieve the same result.&lt;/p&gt;

&lt;p&gt;Now add a few useful aliases to the end of &lt;code&gt;~/.zshrc&lt;/code&gt; or &lt;code&gt;~/.bashrc&lt;/code&gt;, depending on your shell:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# DevOps aliases&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;ll&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'ls -lah'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;ports&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'sudo ss -tulpn'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;logs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'sudo journalctl -f'&lt;/span&gt;
&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;c&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'clear'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reload the configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or if you use Bash:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.bashrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't fill your shell with 200 aliases on day one. That looks like productivity, but it's often decoration with side effects. Start with four or five you actually use. Your terminal should help you, not become an airplane cockpit where you don't know which button turns off the engine.&lt;/p&gt;

&lt;h2&gt;
  
  
  Creating the course workspace
&lt;/h2&gt;

&lt;p&gt;Let's leave a clean structure for the next exercises:&lt;br&gt;
&lt;/p&gt;

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

&lt;span class="nb"&gt;touch &lt;/span&gt;notes/lesson-02.md
&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'DevOps course workspace ready\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; notes/lesson-02.md
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check the result (using the &lt;code&gt;ll&lt;/code&gt; alias you just created):&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;You should see something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;drwxr-xr-x  7 user user 4.0K Jul  6 10:00 .
drwxr-x--- 20 user user 4.0K Jul  6 10:00 ..
drwxr-xr-x  2 user user 4.0K Jul  6 10:00 configs
drwxr-xr-x  2 user user 4.0K Jul  6 10:00 logs
drwxr-xr-x  2 user user 4.0K Jul  6 10:00 notes
drwxr-xr-x  2 user user 4.0K Jul  6 10:00 projects
drwxr-xr-x  2 user user 4.0K Jul  6 10:00 scripts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now initialize a Git repository to version your notes and scripts:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;On branch main

No commits yet

Untracked files:
  notes/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Make the first commit. First, tell Git who you are (if this is your first time using Git on this machine):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git config user.email &lt;span class="s2"&gt;"you@email.com"&lt;/span&gt;
git config user.name &lt;span class="s2"&gt;"Your Name"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;--global&lt;/code&gt; if you want this configuration applied to all your repositories.&lt;/p&gt;

&lt;p&gt;Now for the actual commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git add notes/lesson-02.md
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"chore: initialize devops course workspace"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, even for a learning workspace. Especially for a learning workspace. If you break something, Git gives you a way back without doing archaeology in your own directory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final check
&lt;/h2&gt;

&lt;p&gt;Before closing, run this small checklist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;pwd
&lt;/span&gt;git &lt;span class="nt"&gt;--version&lt;/span&gt;
ssh &lt;span class="nt"&gt;-V&lt;/span&gt;
curl &lt;span class="nt"&gt;--version&lt;/span&gt;
jq &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And confirm mentally:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;I have a usable Linux terminal;&lt;/li&gt;
&lt;li&gt;I have a course directory;&lt;/li&gt;
&lt;li&gt;I can install tools;&lt;/li&gt;
&lt;li&gt;Git works;&lt;/li&gt;
&lt;li&gt;I know how to connect through SSH, or at least I understand what I need to do it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of this fails, don't continue as if nothing happened. That's one of the big DevOps lessons: a half-broken environment today becomes a lost afternoon tomorrow. Like a chair with one loose leg: you can sit on it, yes, but part of your brain is already drafting the incident report.&lt;/p&gt;




&lt;p&gt;Your environment is no longer “an open terminal where things happen.” You now have a minimal workshop: a shell, base tools, a working structure, Git, and a clear idea of how to enter a remote machine.&lt;/p&gt;

&lt;p&gt;In the next lesson we'll go down into the Linux filesystem: directories, files, permissions, and basic commands. That's where &lt;code&gt;/etc&lt;/code&gt;, &lt;code&gt;/var&lt;/code&gt;, &lt;code&gt;/home&lt;/code&gt;, and friends stop looking like names chosen by someone in a hurry.&lt;/p&gt;

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

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Introduction to the DevOps world: culture, automation, and feedback</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Wed, 21 Oct 2026 06:50:40 +0000</pubDate>
      <link>https://dev.to/fj_palacios/introduction-to-the-devops-world-culture-automation-and-feedback-j14</link>
      <guid>https://dev.to/fj_palacios/introduction-to-the-devops-world-culture-automation-and-feedback-j14</guid>
      <description>&lt;p&gt;It's 5:43 PM on a Thursday. Development has just finished a feature. Operations needs to deploy it. The feature works locally, tests are green, the ticket is Done, and someone is already thinking about closing the laptop with dignity.&lt;/p&gt;

&lt;p&gt;Then production shows up and says: “adorable, but no.”&lt;/p&gt;

&lt;p&gt;The server has a different Node version. The environment variable is missing. The deploy script depends on a manual step known by exactly one person. Logs live on one machine, configuration on another, and the document explaining the process is called &lt;code&gt;deploy-final-v2-good-now-for-real.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Whose fault is it? Dev? Ops? The VPN? That &lt;code&gt;README&lt;/code&gt; nobody has updated since 2021? If you're just starting out, this may sound exaggerated. If you've worked in software before, you probably just remembered a specific face. Sorry about that.&lt;/p&gt;

&lt;p&gt;Welcome to the DevOps world.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem before DevOps
&lt;/h2&gt;

&lt;p&gt;For a long time, software delivery was organized around a very clear separation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Development&lt;/strong&gt; wrote the code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Operations&lt;/strong&gt; managed servers, networks, deployments, and production.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On paper, that sounds reasonable. Like separating kitchen and service in a restaurant: one team prepares the food, another serves it. The problem starts when the kitchen doesn't know how the dish is served, service doesn't know what's in the dish, and the customer is staring at lasagna that's still frozen in the middle.&lt;/p&gt;

&lt;p&gt;In software, that separation produced familiar symptoms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;“It works on my machine.”&lt;/li&gt;
&lt;li&gt;“Ops handles deployments, ask them.”&lt;/li&gt;
&lt;li&gt;“We can't deploy until Friday because there's a change window.”&lt;/li&gt;
&lt;li&gt;“Where are the logs?”&lt;/li&gt;
&lt;li&gt;“Who touched production?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;“It works on my machine” is practically cultural heritage in this industry. It solves nothing technically, but it does buy three seconds of awkward silence. Like assembling IKEA furniture, finding one spare metal part, and deciding it was probably decorative. The shelf is standing. For now.&lt;/p&gt;

&lt;p&gt;The real problem wasn't that Dev was irresponsible or Ops was slow. That caricature is convenient, but wrong. The problem was &lt;strong&gt;the system of work&lt;/strong&gt;: separated teams, different goals, little automation, late feedback, and deployments treated as special events instead of a normal part of development.&lt;/p&gt;

&lt;p&gt;That's where DevOps comes in.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;DevOps&lt;/strong&gt; is a culture and a set of practices for delivering software faster, more safely, and more reliably through collaboration, automation, and continuous feedback.&lt;/p&gt;

&lt;p&gt;The important sentence is this: &lt;strong&gt;DevOps is not a tool&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;It is not Jenkins. It is not Docker. It is not Kubernetes. It is not Terraform. It is not having a 400-line YAML file and hoping that if you look at it respectfully enough, it won't fail.&lt;/p&gt;

&lt;p&gt;Tools help, of course. But DevOps starts earlier: it starts when development and operations stop working like two departments throwing packages over a fence and start sharing responsibility for the whole software lifecycle.&lt;/p&gt;

&lt;p&gt;That full lifecycle looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Plan → Code → Build → Test → Release → Deploy → Operate → Monitor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DevOps tries to make that cycle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;faster&lt;/strong&gt;, because repetitive steps are automated;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;safer&lt;/strong&gt;, because manual changes and surprises are reduced;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;more observable&lt;/strong&gt;, because you know what is happening when something breaks;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;more collaborative&lt;/strong&gt;, because the team shares context and responsibility.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The uncomfortable bit: DevOps cannot be “installed.” It has to be practiced. If a company says “we bought DevOps,” what it probably bought is a tool, consulting, or a laptop sticker. Real DevOps changes how work happens.&lt;/p&gt;

&lt;p&gt;And to change how work happens, you need three core ideas.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three core ideas: collaboration, automation, and feedback
&lt;/h2&gt;

&lt;p&gt;We could build a giant list of principles, but for now keep three. They are the tripod. If one leg fails, the camera falls and the picture comes out blurry. Humble metaphor, accurate result.&lt;/p&gt;

&lt;h3&gt;
  
  
  Collaboration
&lt;/h3&gt;

&lt;p&gt;Dev and Ops should not meet for the first time on deployment day.&lt;/p&gt;

&lt;p&gt;Collaboration means that people writing the code understand how it runs in production, and people operating the system understand what is changing in the product. Not so everyone does everything all the time — that ends in calendar-shaped chaos — but so there are no absurd walls.&lt;/p&gt;

&lt;p&gt;A developer doesn't need to become a BGP networking expert to deploy an API. But they should understand what variables it needs, what ports it exposes, what logs it emits, and how to tell if it is healthy.&lt;/p&gt;

&lt;p&gt;An operations person doesn't need to know every detail of the business domain. But they should be involved in decisions that affect deployment, capacity, observability, and recovery.&lt;/p&gt;

&lt;p&gt;DevOps removes the “I already did my part” escape hatch. In real systems, that sentence is a trap. Your part does not end when you run &lt;code&gt;git push&lt;/code&gt;; it ends when the software delivers value in a real environment.&lt;/p&gt;

&lt;h3&gt;
  
  
  Automation
&lt;/h3&gt;

&lt;p&gt;Every repeated manual step is a future incident updating its résumé.&lt;/p&gt;

&lt;p&gt;Building by hand, copying files over SSH, restarting services “in the correct order,” editing configuration directly in production... all of that can work once. You can also cross a road with your eyes closed once. That doesn't make it a methodology.&lt;/p&gt;

&lt;p&gt;Automation turns fragile rituals into repeatable steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Code → pipeline → tests → build → deploy → monitoring
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead of depending on someone remembering the exact ceremony, the process lives in scripts, pipelines, repositories, and infrastructure as code.&lt;/p&gt;

&lt;p&gt;This is where tools we'll cover later enter the picture: Git, Jenkins, Docker, Kubernetes, Terraform, Prometheus, Grafana. But notice the mental order: &lt;strong&gt;problem first, tool second&lt;/strong&gt;. Buying Kubernetes before understanding deployment is like buying an excavator because you want to hang a picture frame.&lt;/p&gt;

&lt;h3&gt;
  
  
  Feedback
&lt;/h3&gt;

&lt;p&gt;Fast feedback prevents errors from arriving late, expensive, and dressed as production.&lt;/p&gt;

&lt;p&gt;Automated tests, pipelines, alerts, logs, metrics, dashboards, postmortems: all of this exists to answer questions as early as possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the change work?&lt;/li&gt;
&lt;li&gt;Is the system still healthy?&lt;/li&gt;
&lt;li&gt;Did the deploy break something?&lt;/li&gt;
&lt;li&gt;Are users suffering?&lt;/li&gt;
&lt;li&gt;Can we recover quickly?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without feedback, you work blind. And working blind in infrastructure has a technical name: Friday afternoon.&lt;/p&gt;

&lt;p&gt;With collaboration, automation, and feedback, you can already see why DevOps is not “put someone in charge of Jenkins.” It is a way to reduce distance between writing software and operating it.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, is DevOps a role?
&lt;/h2&gt;

&lt;p&gt;Here is where the confusion gets fun. Fun in the sense of “some org charts look generated by a sleep-deprived AI.”&lt;/p&gt;

&lt;p&gt;DevOps started as culture and practices, not a job title. Then the industry did what the industry does: it turned the idea into job postings.&lt;/p&gt;

&lt;p&gt;Today you'll see titles like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DevOps Engineer&lt;/li&gt;
&lt;li&gt;Site Reliability Engineer&lt;/li&gt;
&lt;li&gt;Platform Engineer&lt;/li&gt;
&lt;li&gt;Cloud Engineer&lt;/li&gt;
&lt;li&gt;Infrastructure Engineer&lt;/li&gt;
&lt;li&gt;Build and Release Engineer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Are they the same? No. Do they overlap? A lot. Does it depend on the company? Absolutely. Does that help beginners? Not even slightly.&lt;/p&gt;

&lt;p&gt;A practical way to frame it:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Focus&lt;/th&gt;
&lt;th&gt;Main question&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;DevOps&lt;/td&gt;
&lt;td&gt;How do we deliver software better and with less friction?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SRE&lt;/td&gt;
&lt;td&gt;How do we make the system reliable in a measurable way?&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Platform Engineering&lt;/td&gt;
&lt;td&gt;How do we build a platform so other teams deliver software better?&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;DevOps usually focuses on collaboration, automation, CI/CD, and infrastructure. SRE takes many of those ideas into operational reliability: SLOs, error budgets, incident response, toil, postmortems. Platform Engineering climbs another level: internal tools, Golden Paths, and self-service so teams don't reinvent deployment in every repository.&lt;/p&gt;

&lt;p&gt;If DevOps reduces the wall between Dev and Ops, SRE puts numbers on reliability, and Platform Engineering builds the road teams drive on. If the road is well built, nobody thinks about it. If it isn't, everyone ends up in an infinite roundabout holding a Jira ticket.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a traditional DevOps Engineer does
&lt;/h2&gt;

&lt;p&gt;In many companies, the DevOps Engineer role combines several responsibilities:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Designing and maintaining &lt;strong&gt;CI/CD pipelines&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Automating &lt;strong&gt;deployments&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Managing &lt;strong&gt;infrastructure as code&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Maintaining servers, environments, and configuration.&lt;/li&gt;
&lt;li&gt;Setting up basic monitoring.&lt;/li&gt;
&lt;li&gt;Helping teams deploy more safely.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means touching many moving parts. Some pleasant. Some YAML-shaped.&lt;/p&gt;

&lt;p&gt;The usual skills include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Linux/Unix&lt;/strong&gt;, because most servers live there.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terminal and scripting&lt;/strong&gt;, especially Bash and sometimes Python.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Git&lt;/strong&gt;, because without version control there is no traceability. If Git still feels like a black box with branches, &lt;a href="https://dev.to/en/courses/mastering-git-from-scratch"&gt;Mastering Git from Scratch&lt;/a&gt; will save you real pain.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Containers&lt;/strong&gt;, usually Docker. If you want to enter there before Kubernetes, &lt;a href="https://dev.to/en/courses/mastering-docker-from-scratch"&gt;Mastering Docker from Scratch&lt;/a&gt; is the natural path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud providers&lt;/strong&gt;, like AWS or GCP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;, so you can see what is happening when the system stops smiling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key is not knowing all of this on day one. No serious person expects that. The key is understanding the map: which piece solves which problem and how the pieces talk to each other.&lt;/p&gt;

&lt;p&gt;Because if you don't have a map, every tool looks like a universal solution. That's how you end up trying to fix a DNS problem with Terraform. Don't ask how I know.&lt;/p&gt;

&lt;h2&gt;
  
  
  The DevOps ecosystem
&lt;/h2&gt;

&lt;p&gt;A simple way to understand the ecosystem is to follow a change from the moment it is written to the moment it is observed in production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Development → Build → Test → Deploy → Monitor

Git → Jenkins → Docker → Kubernetes → Prometheus
      GitHub    Registry   Helm         Grafana
      GitLab    Artifacts  ArgoCD       Alerts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Don't memorize all the names yet. Seriously, don't stress. The list looks like a hardware store where someone mixed screwdrivers, saws, cables, and a welding machine in the same aisle.&lt;/p&gt;

&lt;p&gt;What matters is understanding the categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Version control&lt;/strong&gt;: where code and change history live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CI/CD&lt;/strong&gt;: how build, tests, and deploy are automated.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Packaging&lt;/strong&gt;: how the application is packaged to run consistently across environments.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration&lt;/strong&gt;: how multiple services and their lifecycle are managed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Infrastructure as Code&lt;/strong&gt;: how infrastructure is described in a versioned way.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Observability&lt;/strong&gt;: how you see what is happening inside the system.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each category answers a different question. That distinction saves you from a classic trap: learning tools without understanding the problem. That produces long résumés and fragile systems. Lots of technology, little judgment. Like buying a professional toolbox to tighten one screw and ending up disassembling the washing machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The course roadmap
&lt;/h2&gt;

&lt;p&gt;This course is designed so the map appears gradually, not as a wall of acronyms on day one.&lt;/p&gt;

&lt;p&gt;We'll move through three large phases.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 1: Traditional DevOps
&lt;/h3&gt;

&lt;p&gt;First we'll build fundamentals:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux and terminal.&lt;/li&gt;
&lt;li&gt;Processes, services, logs, and basic networking.&lt;/li&gt;
&lt;li&gt;Bash scripting.&lt;/li&gt;
&lt;li&gt;Git applied to infrastructure.&lt;/li&gt;
&lt;li&gt;CI/CD with Jenkins.&lt;/li&gt;
&lt;li&gt;Docker and containers.&lt;/li&gt;
&lt;li&gt;Kubernetes.&lt;/li&gt;
&lt;li&gt;Terraform.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This phase answers the question: &lt;strong&gt;how do I automate and operate the software delivery lifecycle?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here you'll get your hands dirty. Good. DevOps without touching a terminal is like learning to swim from a PowerPoint deck.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 2: SRE
&lt;/h3&gt;

&lt;p&gt;Then we'll move into Site Reliability Engineering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Advanced observability.&lt;/li&gt;
&lt;li&gt;SLOs and error budgets.&lt;/li&gt;
&lt;li&gt;Incident management.&lt;/li&gt;
&lt;li&gt;Blameless postmortems.&lt;/li&gt;
&lt;li&gt;Chaos engineering.&lt;/li&gt;
&lt;li&gt;Capacity planning.&lt;/li&gt;
&lt;li&gt;Sustainable on-call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This phase asks a different question: &lt;strong&gt;how do I make the system reliable without turning the team into a permanent fire brigade?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;SRE introduces a fundamental idea: reliability with numbers. Not “we want it to work well,” but “this service must meet this SLO, and this is the acceptable error margin.” Less heroic intuition, more engineering.&lt;/p&gt;

&lt;h3&gt;
  
  
  Phase 3: Platform Engineering
&lt;/h3&gt;

&lt;p&gt;Finally we reach Platform Engineering:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Golden Paths.&lt;/li&gt;
&lt;li&gt;Developer Experience.&lt;/li&gt;
&lt;li&gt;Self-service.&lt;/li&gt;
&lt;li&gt;Backstage.&lt;/li&gt;
&lt;li&gt;Service catalog.&lt;/li&gt;
&lt;li&gt;Internal Developer Platforms.&lt;/li&gt;
&lt;li&gt;Internal tooling.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This phase answers the mature question: &lt;strong&gt;how do I build a platform so other teams deliver software better without depending on me for every step?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That shift is huge. You move from putting out fires to designing systems that prevent fires from starting. Or, more realistically, systems that at least keep the fire in a controlled area, with alerts, a runbook, and an extinguisher that didn't expire in 2019.&lt;/p&gt;

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

&lt;p&gt;DevOps is not about tools. It is about reducing friction between building software and operating it in a real environment.&lt;/p&gt;

&lt;p&gt;Tools matter, but they are the consequence. Git, Jenkins, Docker, Kubernetes, Terraform, Prometheus, or Backstage make sense when you understand what problem they solve. Without that context, they are just expensive names in an architecture nobody wants to touch.&lt;/p&gt;

&lt;p&gt;Keep these ideas:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DevOps emerged to break silos between Development and Operations.&lt;/li&gt;
&lt;li&gt;The foundation is collaboration, automation, and fast feedback.&lt;/li&gt;
&lt;li&gt;DevOps is not a pure role, even though roles called DevOps Engineer exist.&lt;/li&gt;
&lt;li&gt;SRE and Platform Engineering are related evolutions, not magic words.&lt;/li&gt;
&lt;li&gt;The course will move from real fundamentals to modern internal platforms, without jumping straight into deep YAML.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Think of a real or imaginary project you know. Write a short note describing how a change is delivered today: who writes the code, how it is tested, how it is deployed, where logs are checked, and how the team knows it works. If at any point your answer is “I don't know” or “someone does it manually,” perfect: you just found a future lesson in this course.&lt;/p&gt;

&lt;p&gt;In the next tutorial we'll prepare the working environment. We'll compare local virtual machines, cloud environments, and WSL2, then set up a Linux base so you can start using the terminal without relying on dark magic or “it was already configured on my laptop.”&lt;/p&gt;

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

</description>
      <category>devops</category>
      <category>cicd</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Marks and jumps in Vim: navigate without losing the thread</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Tue, 20 Oct 2026 12:31:21 +0000</pubDate>
      <link>https://dev.to/fj_palacios/marks-and-jumps-in-vim-navigate-without-losing-the-thread-2a1p</link>
      <guid>https://dev.to/fj_palacios/marks-and-jumps-in-vim-navigate-without-losing-the-thread-2a1p</guid>
      <description>&lt;p&gt;You're editing a function at line 347. Before finishing, you need to check how another function works. You jump with &lt;code&gt;/&lt;/code&gt;, get there, read it, realize you also need to see a constant you defined near the top. You head up there. When you try to get back to line 347 — you can't find it. You search for the function name, land close, move around a bit. You pick up the thread again. Now you need to check one more thing in another file.&lt;/p&gt;

&lt;p&gt;This isn't editing anymore. This is sightseeing.&lt;/p&gt;

&lt;p&gt;Vim has a full system for remembering where you've been: marks for manually saving positions, a jump list for the automatic history of your movements, and a changelist for the spots where you made edits. Once you work these into your flow, "where was I?" stops being a question.&lt;/p&gt;

&lt;h2&gt;
  
  
  Marks: leaving a signal before you go
&lt;/h2&gt;

&lt;p&gt;A mark is a saved position. To create one, use &lt;code&gt;m&lt;/code&gt; followed by a letter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="k"&gt;ma&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That saves your current position under the key &lt;code&gt;a&lt;/code&gt;. You can use any lowercase letter (a-z). Now move wherever you need to — another function, another section, another file. To return exactly where you were, use backtick followed by the letter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;`&lt;span class="k"&gt;a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cursor back to that exact line and column.&lt;/p&gt;

&lt;p&gt;There's also the apostrophe version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;'&lt;span class="k"&gt;a&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference: &lt;code&gt;`a&lt;/code&gt; (backtick) jumps to the exact position — line &lt;strong&gt;and&lt;/strong&gt; column. &lt;code&gt;'a&lt;/code&gt; (apostrophe) jumps to the line, but to the first non-blank character. It's the difference between remembering "it was in the third drawer, under the old batteries, in the right corner" versus just "the third drawer." Both work. One is more precise.&lt;/p&gt;

&lt;p&gt;Which one to use? Backtick. Almost always. Unless you specifically need the first character of the line, backtick puts you back exactly where you left the cursor.&lt;/p&gt;

&lt;p&gt;Marks are movements too, which means they compose with &lt;a href="https://dev.to/en/tutorials/normal-mode-power-of-vim"&gt;Normal mode grammar&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="k"&gt;d&lt;/span&gt;`&lt;span class="k"&gt;a&lt;/span&gt;    " &lt;span class="k"&gt;delete&lt;/span&gt; from &lt;span class="nb"&gt;cursor&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; &lt;span class="k"&gt;mark&lt;/span&gt; &lt;span class="k"&gt;a&lt;/span&gt;
&lt;span class="k"&gt;y&lt;/span&gt;'&lt;span class="k"&gt;b&lt;/span&gt;    " &lt;span class="k"&gt;yank&lt;/span&gt; from &lt;span class="nb"&gt;cursor&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; the &lt;span class="nb"&gt;line&lt;/span&gt; of &lt;span class="k"&gt;mark&lt;/span&gt; &lt;span class="k"&gt;b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vim gives you 26 local marks (a-z) and 26 global ones (A-Z) — fifty-two total. Exactly the number that makes you think you need a naming convention and maybe a spreadsheet to track them all. What you'll actually use is two or three: &lt;code&gt;ma&lt;/code&gt; to mark where you jumped from, &lt;code&gt;mb&lt;/code&gt; for something you need to check later, and &lt;code&gt;ms&lt;/code&gt; for that other thing you've been putting off. Always the same two or three.&lt;/p&gt;

&lt;h2&gt;
  
  
  Global marks: across files
&lt;/h2&gt;

&lt;p&gt;Uppercase letters (A-Z) are &lt;strong&gt;global&lt;/strong&gt; marks: they work across files and persist between Vim sessions.&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;That saves the current position under &lt;code&gt;A&lt;/code&gt;. Tomorrow, after closing and reopening Vim, &lt;code&gt;`A&lt;/code&gt; takes you right back there — opening the file if needed.&lt;/p&gt;

&lt;p&gt;Useful when you're constantly jumping between two or three files: &lt;code&gt;mA&lt;/code&gt; in the config file, &lt;code&gt;mB&lt;/code&gt; in the routes file, &lt;code&gt;mC&lt;/code&gt; in the main entry point. You jump between them without searching for anything.&lt;/p&gt;

&lt;p&gt;Honest limitation: only one slot per letter. If you use &lt;code&gt;mA&lt;/code&gt; in a different file, the previous &lt;code&gt;A&lt;/code&gt; is gone. Fine with three files; not the right tool if you're trying to build a full project index with 26 active entries. For navigating many files, Vim has better tools you'll see later in the course.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic marks: the ones Vim sets for you
&lt;/h2&gt;

&lt;p&gt;Vim also saves certain positions automatically. You don't have to do anything — they're already there when you need them.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Mark&lt;/th&gt;
&lt;th&gt;What it stores&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;`.&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Position of the last change in the file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;`^&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Position where you left Insert mode last time&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;`[&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start of the last yanked or changed text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;`]&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;End of the last yanked or changed text&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;`&amp;lt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Start of the last visual selection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;`&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;End of the last visual selection&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The most useful one day-to-day: &lt;code&gt;`.&lt;/code&gt; jumps to the last place you made a change. No setup, no thinking. &lt;code&gt;`.&lt;/code&gt; and you're there.&lt;/p&gt;

&lt;p&gt;Then there's &lt;code&gt;''&lt;/code&gt; (two apostrophes), or &lt;code&gt;&lt;/code&gt;&lt;code&gt;&lt;/code&gt; (two backticks) for the exact-position version. Both store where you were &lt;strong&gt;before your last big jump&lt;/strong&gt; — a search, &lt;code&gt;gg&lt;/code&gt;, &lt;code&gt;G&lt;/code&gt;, jumping to a mark. It's the retroactive "take me back to the start" command. You don't activate it in advance; Vim records it automatically on every large jump.&lt;/p&gt;

&lt;p&gt;To see all active marks at any point:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;marks&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It shows a table with name, line, column, and the text at that line. Useful when you've been working for a while and can't remember which mark points where — which is exactly when you need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The jump list: the editor's history
&lt;/h2&gt;

&lt;p&gt;Beyond manual marks, Vim keeps an automatic position history called the &lt;strong&gt;jump list&lt;/strong&gt;. Every large jump — searching with &lt;code&gt;/&lt;/code&gt;, going to a mark, &lt;code&gt;gg&lt;/code&gt;, &lt;code&gt;G&lt;/code&gt;, jumping to a definition — gets recorded.&lt;/p&gt;

&lt;p&gt;To navigate that history:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;Ctrl&lt;span class="p"&gt;+&lt;/span&gt;&lt;span class="k"&gt;o&lt;/span&gt;    " &lt;span class="k"&gt;go&lt;/span&gt; back &lt;span class="p"&gt;(&lt;/span&gt;older jump&lt;span class="p"&gt;)&lt;/span&gt;
Ctrl&lt;span class="p"&gt;+&lt;/span&gt;&lt;span class="k"&gt;i&lt;/span&gt;    " &lt;span class="k"&gt;go&lt;/span&gt; forward &lt;span class="p"&gt;(&lt;/span&gt;newer jump&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Ctrl+o&lt;/code&gt; is your browser's back button, but for positions inside the editor. If you've made four jumps from where you started, four &lt;code&gt;Ctrl+o&lt;/code&gt; presses bring you back one step at a time. &lt;code&gt;Ctrl+i&lt;/code&gt; goes the other way.&lt;/p&gt;

&lt;p&gt;To see the full list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;jumps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The jump list holds 100 positions. A hundred. Just enough to make it look like something you need to actively manage — you don't. When it fills up, the oldest entry quietly disappears. Vim doesn't send a notification. You keep editing.&lt;/p&gt;

&lt;p&gt;A note about &lt;a href="https://dev.to/en/tutorials/search-and-find-vim"&gt;searches with &lt;code&gt;/&lt;/code&gt; and &lt;code&gt;?&lt;/code&gt;&lt;/a&gt;: every jump to a result registers in the jump list. If you move through six matches with &lt;code&gt;n&lt;/code&gt;, you have six entries. &lt;code&gt;Ctrl+o&lt;/code&gt; undoes the trail.&lt;/p&gt;

&lt;h2&gt;
  
  
  The changelist: where you edited
&lt;/h2&gt;

&lt;p&gt;The jump list tracks positions. The &lt;strong&gt;changelist&lt;/strong&gt; tracks specifically the spots where you made a change in the file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="k"&gt;g&lt;/span&gt;;    " &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; older &lt;span class="k"&gt;change&lt;/span&gt;
&lt;span class="k"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;    " &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; newer &lt;span class="k"&gt;change&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To see the full list:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;changes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The common case: you edited something, jumped to another spot to check a variable, and now you want to get back exactly to where you were typing. &lt;code&gt;g;&lt;/code&gt; and you're there.&lt;/p&gt;

&lt;p&gt;The changelist stores the last 100 changes with exact positions — a detailed audit trail of everything you've touched in the file since you opened it. In practice, you'll use &lt;code&gt;g;&lt;/code&gt; once, to get back to the last place you typed something. That's what it's there for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Special jumps
&lt;/h2&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;%&lt;/code&gt;: the matching pair
&lt;/h3&gt;

&lt;p&gt;Place the cursor on a parenthesis, brace, or bracket, and press &lt;code&gt;%&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;It jumps to the matching character. On &lt;code&gt;(&lt;/code&gt; → jumps to &lt;code&gt;)&lt;/code&gt;. On &lt;code&gt;}&lt;/code&gt; → jumps to the &lt;code&gt;{&lt;/code&gt; that opens it. You can bounce back and forth with repeated &lt;code&gt;%&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Useful in heavily nested code: instead of visually counting where a block ends, &lt;code&gt;%&lt;/code&gt; takes you there directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;gd&lt;/code&gt; and &lt;code&gt;gD&lt;/code&gt;: go to definition
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight viml"&gt;&lt;code&gt;&lt;span class="nb"&gt;gd&lt;/span&gt;    " &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; local definition &lt;span class="p"&gt;(&lt;/span&gt;within the current scope&lt;span class="p"&gt;)&lt;/span&gt;
gD    " &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;to&lt;/span&gt; global definition &lt;span class="p"&gt;(&lt;/span&gt;from the &lt;span class="nb"&gt;top&lt;/span&gt; of the &lt;span class="k"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;gd&lt;/code&gt; finds the first occurrence of the word under the cursor within the local scope — useful for jumping to where a variable is declared. &lt;code&gt;gD&lt;/code&gt; does the same but searches from the beginning of the file.&lt;/p&gt;

&lt;p&gt;Not the same as LSP, which you'll see later in the course and which jumps with actual semantic context. &lt;code&gt;gd&lt;/code&gt; and &lt;code&gt;gD&lt;/code&gt; are text searches. In files without LSP, or for local variables with clear names, they work without any configuration.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;gf&lt;/code&gt;: open the file under the cursor
&lt;/h3&gt;

&lt;p&gt;The most underrated command in this lesson.&lt;/p&gt;

&lt;p&gt;Place your cursor over a file path in the text:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../config/app&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./utils/format&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And press:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Vim opens the file. No extra commands, no file explorer, no copying the path and pasting it into &lt;code&gt;:e&lt;/code&gt;. If it exists, &lt;code&gt;gf&lt;/code&gt; goes there. To come back, use &lt;code&gt;Ctrl+o&lt;/code&gt; — the jump was already recorded in the jump list.&lt;/p&gt;

&lt;p&gt;One caveat: &lt;code&gt;gf&lt;/code&gt; works with the literal text under the cursor. Paths using bundler aliases (&lt;code&gt;@/components&lt;/code&gt;) or implicit extensions might need extra configuration to resolve. On explicit paths, it works out of the box.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;m{a-z}&lt;/code&gt; creates a local mark; &lt;code&gt;`{letter}&lt;/code&gt; jumps to the exact line and column; &lt;code&gt;'{letter}&lt;/code&gt; jumps to the line&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;m{A-Z}&lt;/code&gt; creates a global mark that persists across sessions and works across files&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;`.&lt;/code&gt; jumps to the last change; &lt;code&gt;''&lt;/code&gt; jumps to the position before the last big jump&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;:marks&lt;/code&gt; lists all active marks&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Ctrl+o&lt;/code&gt; / &lt;code&gt;Ctrl+i&lt;/code&gt; move backward and forward through the jump list&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;g;&lt;/code&gt; / &lt;code&gt;g,&lt;/code&gt; move through the changelist (edit history)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;%&lt;/code&gt; jumps between matching open and close characters&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gd&lt;/code&gt; / &lt;code&gt;gD&lt;/code&gt; jump to the local or global first occurrence of the word under the cursor&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;gf&lt;/code&gt; opens the file whose path is under the cursor&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Open a large file from a real project. Set &lt;code&gt;ma&lt;/code&gt; where you are, jump to another section with &lt;code&gt;/&lt;/code&gt;, come back with &lt;code&gt;`a&lt;/code&gt;. Make a small edit, jump with &lt;code&gt;gg&lt;/code&gt;, come back with &lt;code&gt;g;&lt;/code&gt;. If there's any file path referenced in the code, place your cursor on it and try &lt;code&gt;gf&lt;/code&gt;. Then run &lt;code&gt;:marks&lt;/code&gt; and &lt;code&gt;:jumps&lt;/code&gt; to see both lists — everything you just did is recorded there.&lt;/p&gt;

&lt;p&gt;Marks and jumps close out Vim's core navigation system. You now have the tools to move through any codebase without losing the thread. In the next tutorial, we'll cover &lt;strong&gt;text objects&lt;/strong&gt; — what many consider Vim's actual superpower: the ability to operate on "the content inside these quotes," "the block within these braces," or "this whole word" with a single two-key command.&lt;/p&gt;

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

</description>
      <category>vim</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Docker Compose file structure: health checks, environment variables, and restart</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Mon, 19 Oct 2026 10:03:37 +0000</pubDate>
      <link>https://dev.to/fj_palacios/docker-compose-file-structure-health-checks-environment-variables-and-restart-1ak7</link>
      <guid>https://dev.to/fj_palacios/docker-compose-file-structure-health-checks-environment-variables-and-restart-1ak7</guid>
      <description>&lt;p&gt;The &lt;code&gt;docker-compose.yml&lt;/code&gt; from the last lesson had a subtle lie built into it. &lt;code&gt;depends_on: db&lt;/code&gt; sounds like "wait for Postgres to be ready." What it actually means is "wait for the Postgres container to be running." These are not the same thing.&lt;/p&gt;

&lt;p&gt;On your machine, Postgres initializes in under a second and you never notice the difference. On a CI runner with a slower disk, your API tries to connect to the database while Postgres is still writing its initial data directory. The connection fails, the container restarts, and you're left with an intermittent error you can't reproduce locally — the worst kind of bug to debug.&lt;/p&gt;

&lt;p&gt;The previous tutorial mentioned &lt;code&gt;condition: service_healthy&lt;/code&gt; as the real solution and left it there. This lesson covers it in full, along with environment variables from files and restart policies. Three additions that take a &lt;code&gt;docker-compose.yml&lt;/code&gt; from "works on my machine" to "works everywhere."&lt;/p&gt;

&lt;h2&gt;
  
  
  Every field a service can have
&lt;/h2&gt;

&lt;p&gt;Before diving into health checks, here's a reference of what a service definition can include. The previous lesson used &lt;code&gt;image&lt;/code&gt;, &lt;code&gt;ports&lt;/code&gt;, &lt;code&gt;environment&lt;/code&gt;, &lt;code&gt;depends_on&lt;/code&gt;, and &lt;code&gt;volumes&lt;/code&gt;. There are more:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Image or build (not both)&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp/api:latest&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;context&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt; &lt;span class="c1"&gt;# Directory with the Dockerfile&lt;/span&gt;
      &lt;span class="na"&gt;dockerfile&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Dockerfile&lt;/span&gt; &lt;span class="c1"&gt;# Dockerfile name (default: Dockerfile)&lt;/span&gt;
      &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt; &lt;span class="c1"&gt;# Build-time arguments&lt;/span&gt;

    &lt;span class="c1"&gt;# Optional: explicit container name (auto-generated if omitted)&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp-api&lt;/span&gt;

    &lt;span class="c1"&gt;# Port mapping: host:container&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;

    &lt;span class="c1"&gt;# Environment variables inline&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;NODE_ENV&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://db:5432/myapp&lt;/span&gt;

    &lt;span class="c1"&gt;# Environment variables from a file&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;

    &lt;span class="c1"&gt;# Dependencies&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;

    &lt;span class="c1"&gt;# Volumes&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;./api/src:/app/src&lt;/span&gt; &lt;span class="c1"&gt;# Bind mount (development hot reload)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;api-data:/app/data&lt;/span&gt; &lt;span class="c1"&gt;# Named volume&lt;/span&gt;

    &lt;span class="c1"&gt;# Networks this service connects to&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;frontend&lt;/span&gt;

    &lt;span class="c1"&gt;# Health check&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-f"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000/health"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;30s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;40s&lt;/span&gt;

    &lt;span class="c1"&gt;# Restart policy&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Not every service needs all of these. Most use five or six fields. This is the full reference — the most relevant ones get detailed treatment in this and the following lessons.&lt;/p&gt;

&lt;p&gt;One note on &lt;code&gt;build&lt;/code&gt;: the short form (&lt;code&gt;build: ./api&lt;/code&gt;) assumes there's a &lt;code&gt;Dockerfile&lt;/code&gt; in that directory. The long form with &lt;code&gt;context&lt;/code&gt;, &lt;code&gt;dockerfile&lt;/code&gt;, and &lt;code&gt;args&lt;/code&gt; is for when you need build arguments or a differently named Dockerfile.&lt;/p&gt;

&lt;h2&gt;
  
  
  Health checks: waiting for the service, not just the container
&lt;/h2&gt;

&lt;p&gt;A health check tells Docker how to verify that a service is actually ready — not just running. Docker runs that command on a schedule and marks the service &lt;code&gt;healthy&lt;/code&gt; when it succeeds consistently.&lt;/p&gt;

&lt;p&gt;For Postgres, the standard health check uses &lt;code&gt;pg_isready&lt;/code&gt;, a utility that ships with the image and returns 0 when the database accepts connections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;myapp&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;myapp"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt; &lt;span class="c1"&gt;# How often Docker runs the check&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt; &lt;span class="c1"&gt;# Max time allowed per check&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt; &lt;span class="c1"&gt;# Fail after this many consecutive failures&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt; &lt;span class="c1"&gt;# Grace period before failures count against retries&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;start_period&lt;/code&gt; is worth understanding: during that window, health check failures don't count toward the &lt;code&gt;retries&lt;/code&gt; limit. This gives Postgres time to finish initializing before Docker starts caring whether the check passes. Without it, a health check could fail during normal startup and prematurely mark the service as &lt;code&gt;unhealthy&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With the health check defined, update &lt;code&gt;depends_on&lt;/code&gt; to use it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt; &lt;span class="c1"&gt;# Wait until db passes its health check&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The syntax changes from a list to a map when you add a condition. &lt;code&gt;condition: service_healthy&lt;/code&gt; holds the API back until Postgres's health check returns &lt;code&gt;healthy&lt;/code&gt;. This is what &lt;code&gt;depends_on: [db]&lt;/code&gt; implied it was doing.&lt;/p&gt;

&lt;p&gt;Redis gets a similarly simple check:&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;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
  &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
    &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And for an HTTP service you control:&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;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;
  &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-sf&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;http://localhost:3000/health&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;||&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;exit&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
    &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
    &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;15s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;|| exit 1&lt;/code&gt; ensures the check fails if &lt;code&gt;curl&lt;/code&gt; can't connect, times out, or gets a non-2xx response. Without it, the shell returns 0 even when &lt;code&gt;curl&lt;/code&gt; reports an error, and Docker happily marks the service healthy when it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Environment variables: the .env file
&lt;/h2&gt;

&lt;p&gt;Three environment variables inline in a &lt;code&gt;docker-compose.yml&lt;/code&gt; are fine. Twelve in staging is manageable. Forty-seven in production, where someone hardcoded a password two years ago and there's a JWT secret nobody remembers generating — that's a different situation.&lt;/p&gt;

&lt;p&gt;The solution is to move variables out of the YAML.&lt;/p&gt;

&lt;h3&gt;
  
  
  Interpolation from .env
&lt;/h3&gt;

&lt;p&gt;Compose automatically reads a &lt;code&gt;.env&lt;/code&gt; file in the project root and makes its variables available for interpolation in the &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# .env&lt;/span&gt;
&lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myapp
&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret
&lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myapp
&lt;span class="nv"&gt;API_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;

  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${API_PORT}:3000"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;${VARIABLE}&lt;/code&gt; syntax pulls values from &lt;code&gt;.env&lt;/code&gt; or the shell environment. If a variable is missing, Compose substitutes an empty string without complaining. To make it fail loudly when a critical variable is absent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;POSTGRES_PASSWORD: &lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;:?POSTGRES_PASSWORD&lt;span class="p"&gt; is required&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file goes in &lt;code&gt;.gitignore&lt;/code&gt;. Always. What you do version is a &lt;code&gt;.env.example&lt;/code&gt; with variable names and placeholder values — so anyone cloning the repo knows what they need to configure.&lt;/p&gt;

&lt;h3&gt;
  
  
  env_file: passing a file directly to the container
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;env_file&lt;/code&gt; is an alternative that passes all variables from a file directly to the container, without declaring them one by one under &lt;code&gt;environment&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;
    &lt;span class="na"&gt;env_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;.env.local&lt;/span&gt; &lt;span class="c1"&gt;# Local overrides, not versioned&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference from interpolation: &lt;code&gt;env_file&lt;/code&gt; injects variables into the container's environment, but they're not available for substitution elsewhere in the YAML. If you need a variable in &lt;code&gt;ports&lt;/code&gt;, &lt;code&gt;image&lt;/code&gt;, or &lt;code&gt;healthcheck&lt;/code&gt;, use interpolation (&lt;code&gt;${VAR}&lt;/code&gt;). If you just need it inside the container and want a cleaner YAML, &lt;code&gt;env_file&lt;/code&gt; is the right choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Restart policies
&lt;/h2&gt;

&lt;p&gt;Without a restart policy, a failed container stays down. In development you'll notice immediately. On a server with no active monitoring, it might be down for hours.&lt;/p&gt;

&lt;p&gt;The four options:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Policy&lt;/th&gt;
&lt;th&gt;Behavior&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;"no"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Never restart automatically (default)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;always&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restart automatically whenever it exits; after a manual &lt;code&gt;docker stop&lt;/code&gt;, restart when the daemon restarts or it is started manually&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;on-failure&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Restart only when the process exits with an error&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;unless-stopped&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Like &lt;code&gt;always&lt;/code&gt;, but respects manual &lt;code&gt;docker compose stop&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For most cases, &lt;code&gt;unless-stopped&lt;/code&gt; is the right call: the service recovers if the process crashes, but you can stop it manually without it coming back on its own.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;always&lt;/code&gt; has a behavior that surprises people the first time: if a container was running when you restarted the server, Docker starts it automatically when the Docker daemon starts. In production, that's usually what you want. On a laptop with ten projects, you'll find Docker starting six development stacks you forgot you had.&lt;/p&gt;

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

&lt;p&gt;Everything together: Node.js API, Postgres, and Redis — with health checks, variables from &lt;code&gt;.env&lt;/code&gt;, custom networks, and restart policies.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;.env&lt;/code&gt; file (add to &lt;code&gt;.gitignore&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myapp
&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;changeme
&lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;myapp
&lt;span class="nv"&gt;REDIS_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;redispass
&lt;span class="nv"&gt;API_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;.env.example&lt;/code&gt; (commit this one):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;REDIS_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="nv"&gt;API_PORT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;docker-compose.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;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_PASSWORD}&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${POSTGRES_DB}&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db-data:/var/lib/postgresql/data&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pg_isready&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-U&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_USER}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-d&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${POSTGRES_DB}"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;5&lt;/span&gt;
      &lt;span class="na"&gt;start_period&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;10s&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis:7-alpine&lt;/span&gt;
    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis-server --requirepass ${REDIS_PASSWORD} --loglevel warning&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
    &lt;span class="na"&gt;healthcheck&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CMD-SHELL"&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis-cli&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;-a&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;${REDIS_PASSWORD}&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;--no-auth-warning&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;ping"&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
      &lt;span class="na"&gt;interval&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;5s&lt;/span&gt;
      &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;3s&lt;/span&gt;
      &lt;span class="na"&gt;retries&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;3&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;${API_PORT}:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB}&lt;/span&gt;
      &lt;span class="na"&gt;REDIS_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis://:${REDIS_PASSWORD}@redis:6379&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
      &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;condition&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;service_healthy&lt;/span&gt;
    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;backend&lt;/span&gt;
    &lt;span class="na"&gt;restart&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;unless-stopped&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;backend&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With this file, &lt;code&gt;docker compose up -d&lt;/code&gt; starts Postgres and Redis, waits for both to pass their health checks, then starts the API. If the API process crashes, &lt;code&gt;unless-stopped&lt;/code&gt; restarts it. Credentials live in &lt;code&gt;.env&lt;/code&gt;, outside the repository.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;networks&lt;/code&gt; field controls what can talk to what. All three services here share &lt;code&gt;backend&lt;/code&gt;, so the API can resolve &lt;code&gt;db&lt;/code&gt; and &lt;code&gt;redis&lt;/code&gt; by name. If you added a frontend service, it would connect to a separate &lt;code&gt;frontend&lt;/code&gt; network — able to reach the API, but not the database directly. Networking in Compose is its own lesson.&lt;/p&gt;




&lt;p&gt;With health checks, externalized environment variables, and restart policies, a &lt;code&gt;docker-compose.yml&lt;/code&gt; stops being a development convenience and starts behaving like real infrastructure. The next lesson goes deep on volumes: how data persists across container restarts, the difference between bind mounts and named volumes, and how to handle backups from Compose itself.&lt;/p&gt;

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




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Take the &lt;code&gt;docker-compose.yml&lt;/code&gt; from the previous lesson's challenge and add a Postgres health check using &lt;code&gt;pg_isready&lt;/code&gt;. Update &lt;code&gt;depends_on&lt;/code&gt; to use &lt;code&gt;condition: service_healthy&lt;/code&gt;. Run &lt;code&gt;docker compose up -d&lt;/code&gt; and watch &lt;code&gt;docker compose ps&lt;/code&gt; as Postgres transitions from &lt;code&gt;starting&lt;/code&gt; to &lt;code&gt;healthy&lt;/code&gt; before the dependent services start.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Introduction to Docker Compose: one file, one command</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Sun, 18 Oct 2026 11:58:37 +0000</pubDate>
      <link>https://dev.to/fj_palacios/introduction-to-docker-compose-one-file-one-command-1jpo</link>
      <guid>https://dev.to/fj_palacios/introduction-to-docker-compose-one-file-one-command-1jpo</guid>
      <description>&lt;p&gt;There's a moment where managing Docker manually stops scaling. Not when you add the second container — that one you can keep in your head. It's when you add the third, and the startup order starts to matter, and the API tries to connect to the database before it's ready, and you have four terminal tabs open, and you're running the same four commands from memory in a slightly different order every time.&lt;/p&gt;

&lt;p&gt;That moment is when Docker Compose starts making sense.&lt;/p&gt;

&lt;p&gt;Instead of remembering which flags you passed to Postgres last week, you describe the entire stack in a single &lt;code&gt;docker-compose.yml&lt;/code&gt; and run one command. The file lives in your repository. The next person who clones it runs &lt;code&gt;docker compose up -d&lt;/code&gt; and gets the same environment you have. No terminal history, no sticky notes, no "I have the command somewhere".&lt;/p&gt;

&lt;h2&gt;
  
  
  What is Docker Compose?
&lt;/h2&gt;

&lt;p&gt;Docker Compose is a tool for defining and running multi-container applications. You describe your services, their relationships, and their configuration in a YAML file. Compose handles creating the network, starting services in the right order, and managing their lifecycle together.&lt;/p&gt;

&lt;p&gt;The approach is declarative: you describe &lt;em&gt;what&lt;/em&gt; your stack looks like, not the step-by-step &lt;em&gt;how&lt;/em&gt; of starting it.&lt;/p&gt;

&lt;p&gt;Here's what running a database, a cache, and an API looked like before Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker network create myapp-network

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; db &lt;span class="nt"&gt;--network&lt;/span&gt; myapp-network &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;secret postgres

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; redis &lt;span class="nt"&gt;--network&lt;/span&gt; myapp-network redis

docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--name&lt;/span&gt; api &lt;span class="nt"&gt;--network&lt;/span&gt; myapp-network &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-p&lt;/span&gt; 3000:3000 &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nv"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;postgresql://db:5432 myapp-api
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four commands. In the right order. With the exact flags. On the right network. And that's three services — not the twelve you'll have in production.&lt;/p&gt;

&lt;p&gt;With Compose:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# docker-compose.yml&lt;/span&gt;
&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;

  &lt;span class="na"&gt;redis&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;

  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp-api&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://db:5432&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&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 compose up &lt;span class="nt"&gt;-d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One file. One command.&lt;/p&gt;

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

&lt;p&gt;Docker Compose v2 ships with Docker Desktop on macOS and Windows. On Linux, it depends on how you installed Docker.&lt;/p&gt;

&lt;p&gt;Check if you already have it:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;If that returns &lt;code&gt;Docker Compose version 5.1.4&lt;/code&gt;, you're set. If it returns an error, or you have the old &lt;code&gt;docker-compose&lt;/code&gt; binary (with a hyphen — the v1 Python tool), install the modern version:&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-compose

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

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

&lt;/div&gt;



&lt;p&gt;The modern command is &lt;code&gt;docker compose&lt;/code&gt; (space, no hyphen). v1 was a separate Python binary, v2 is an official plugin integrated into the Docker CLI. If you installed Docker in 2023 or later, you already have v2.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your first docker-compose.yml
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;docker-compose.yml&lt;/code&gt; has three top-level sections:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# The containers that make up your application&lt;/span&gt;
&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Named persistent storage&lt;/span&gt;
&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="c1"&gt;# Custom networks (optional if the default is enough)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A realistic example — a Node.js API with a Postgres database:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_PASSWORD&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;secret&lt;/span&gt;
      &lt;span class="na"&gt;POSTGRES_DB&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;myapp&lt;/span&gt;
    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db-data:/var/lib/postgresql/data&lt;/span&gt;

  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt; &lt;span class="c1"&gt;# Build from the Dockerfile in ./api&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3000:3000"&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;DATABASE_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgresql://myapp:secret@db:5432/myapp&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt;

&lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db-data&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;image: postgres:16-alpine&lt;/code&gt; pulls a public image, same as &lt;code&gt;docker run&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;build: ./api&lt;/code&gt; builds the image from that directory's Dockerfile. No separate &lt;code&gt;docker build&lt;/code&gt; step needed.&lt;/li&gt;
&lt;li&gt;In &lt;code&gt;DATABASE_URL&lt;/code&gt;, the host is &lt;code&gt;db&lt;/code&gt; — the service name. Compose creates a default network where services resolve each other by name automatically.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;db-data&lt;/code&gt; under &lt;code&gt;volumes:&lt;/code&gt; declares a named volume that Docker manages. Postgres data persists across &lt;code&gt;docker compose down&lt;/code&gt;. The volume stays until you explicitly remove it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The file belongs at the root of your project, versioned alongside your code. If you're using &lt;a href="https://dev.to/en/courses/mastering-git-from-scratch"&gt;Git&lt;/a&gt;, &lt;code&gt;docker-compose.yml&lt;/code&gt; is just another configuration file — and it's exactly what eliminates the mental overhead of remembering how to start the project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Essential commands
&lt;/h2&gt;

&lt;p&gt;These cover the vast majority of what you'll actually need:&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;# Start all services in the background&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt;

&lt;span class="c"&gt;# Start and rebuild images that have a Dockerfile&lt;/span&gt;
docker compose up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--build&lt;/span&gt;

&lt;span class="c"&gt;# Stop containers without removing them&lt;/span&gt;
docker compose stop

&lt;span class="c"&gt;# Stop and remove containers + networks (volumes intact)&lt;/span&gt;
docker compose down

&lt;span class="c"&gt;# Stop and remove everything, including named volumes&lt;/span&gt;
docker compose down &lt;span class="nt"&gt;-v&lt;/span&gt;

&lt;span class="c"&gt;# Show running services and their status&lt;/span&gt;
docker compose ps

&lt;span class="c"&gt;# Show logs from all services&lt;/span&gt;
docker compose logs

&lt;span class="c"&gt;# Follow logs from a specific service&lt;/span&gt;
docker compose logs &lt;span class="nt"&gt;-f&lt;/span&gt; api

&lt;span class="c"&gt;# Run a command inside a running service&lt;/span&gt;
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;api sh
docker compose &lt;span class="nb"&gt;exec &lt;/span&gt;db psql &lt;span class="nt"&gt;-U&lt;/span&gt; myapp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The distinction between &lt;code&gt;stop&lt;/code&gt; and &lt;code&gt;down&lt;/code&gt;: &lt;code&gt;stop&lt;/code&gt; halts containers but keeps them, &lt;code&gt;down&lt;/code&gt; removes them. Named volumes survive &lt;code&gt;down&lt;/code&gt; — they only get removed if you pass &lt;code&gt;-v&lt;/code&gt;. Worth knowing before you accidentally wipe a local database.&lt;/p&gt;

&lt;h2&gt;
  
  
  depends_on: startup ordering
&lt;/h2&gt;

&lt;p&gt;Without &lt;code&gt;depends_on&lt;/code&gt;, Compose starts all services roughly in parallel. That means your API can try to connect to Postgres while Postgres is still initializing — which, depending on how patient your connection retry logic is, will either work on the third attempt or fail immediately with a confusing error.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;db&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;postgres:16-alpine&lt;/span&gt;

  &lt;span class="na"&gt;api&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;./api&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;db&lt;/span&gt; &lt;span class="c1"&gt;# Wait for db container to be running before starting api&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the nuance worth understanding upfront: &lt;code&gt;depends_on&lt;/code&gt; waits for the &lt;em&gt;container&lt;/em&gt; to be running, not for the &lt;em&gt;service&lt;/em&gt; to be ready. The Postgres container can be up and running while Postgres itself is still writing its initial data directory. From Docker's perspective, started. From Postgres's perspective, not yet accepting connections.&lt;/p&gt;

&lt;p&gt;The proper solution — waiting until the service is genuinely healthy — uses health checks with &lt;code&gt;condition: service_healthy&lt;/code&gt;. That's the topic for the next lesson, along with the full &lt;code&gt;docker-compose.yml&lt;/code&gt; structure.&lt;/p&gt;




&lt;p&gt;You now have everything needed to run a real development stack with Docker Compose: a declarative file, the commands to manage it, and the basic dependency model. The next lesson covers the complete &lt;code&gt;docker-compose.yml&lt;/code&gt; structure — environment variables from files, health checks, restart policies, and how to split development and production configuration in the same project.&lt;/p&gt;

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




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Write a &lt;code&gt;docker-compose.yml&lt;/code&gt; with three services: Postgres, Redis, and any image you're familiar with. Start the stack with &lt;code&gt;docker compose up -d&lt;/code&gt;, verify all three services show &lt;code&gt;Up&lt;/code&gt; in &lt;code&gt;docker compose ps&lt;/code&gt;, then follow the combined logs with &lt;code&gt;docker compose logs -f&lt;/code&gt;. When you're done, bring everything down with &lt;code&gt;docker compose down&lt;/code&gt; and confirm &lt;code&gt;docker compose ps&lt;/code&gt; shows nothing running.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>beginners</category>
      <category>tutorial</category>
      <category>devops</category>
    </item>
    <item>
      <title>Git in CI/CD pipelines: from commit to automated deploy</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Sat, 17 Oct 2026 08:53:18 +0000</pubDate>
      <link>https://dev.to/fj_palacios/git-in-cicd-pipelines-from-commit-to-automated-deploy-2gd7</link>
      <guid>https://dev.to/fj_palacios/git-in-cicd-pipelines-from-commit-to-automated-deploy-2gd7</guid>
      <description>&lt;p&gt;The CI runner is the most disoriented machine on the team. It arrives at each run knowing nothing about you: no &lt;code&gt;.gitconfig&lt;/code&gt;, no custom pager, no commit graph precomputed overnight by &lt;code&gt;git maintenance&lt;/code&gt;. An empty container that clones the repository, runs what you tell it, and disappears. Like assembling an IKEA shelf from scratch every time you need to put a book on it — the box arrives complete, no memory of the last time, no leftover screw you set aside just in case. If you know what you're doing, you can pre-stage the pieces for the next build. That's what caching is.&lt;/p&gt;

&lt;p&gt;This is the final lesson in the course. What we'll cover here is how Git fits inside an automated pipeline: what changes compared to local use, how to configure GitHub Actions and GitLab CI so that Git doesn't become the bottleneck, and which practices keep the integration between code and deployment free of surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  Git in a pipeline: what changes
&lt;/h2&gt;

&lt;p&gt;On your machine, Git has context: your global config, your local history, packed objects, configured remotes. On a CI runner, none of that exists. Each run starts from zero and needs to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Authenticate to clone the repository&lt;/li&gt;
&lt;li&gt;Clone (or restore from cache)&lt;/li&gt;
&lt;li&gt;Execute what it needs to execute&lt;/li&gt;
&lt;li&gt;Optionally push or deploy something&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Authentication is what differs most from local use. Locally you use SSH or HTTPS with saved credentials. In CI, platforms inject a temporary token — &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; in GitHub Actions, &lt;code&gt;CI_JOB_TOKEN&lt;/code&gt; in GitLab — that has read permissions (and write if you configure it) only for that run. You don't manage credentials manually: the platform handles it.&lt;/p&gt;

&lt;p&gt;The clone is where there's usually room for improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Shallow clones: the first adjustment to make
&lt;/h2&gt;

&lt;p&gt;By default, &lt;code&gt;git clone&lt;/code&gt; downloads the full repository history. In a project with five years of commits, that can be hundreds of megabytes of objects that the runner will completely ignore — it'll run the tests, generate the build, and forget they ever existed.&lt;/p&gt;

&lt;p&gt;The solution is a shallow clone:&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;# Only fetch the last commit&lt;/span&gt;
git clone &lt;span class="nt"&gt;--depth&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &amp;lt;url&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In GitHub Actions, the &lt;code&gt;actions/checkout&lt;/code&gt; action does this by default (&lt;code&gt;fetch-depth: 1&lt;/code&gt;). In GitLab CI, you configure the &lt;code&gt;GIT_DEPTH&lt;/code&gt; variable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# GitHub Actions — default behavior (shallow)&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

&lt;span class="c1"&gt;# GitHub Actions — full history when you need it&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;

&lt;span class="c1"&gt;# GitLab CI — shallow clone&lt;/span&gt;
&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;GIT_DEPTH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;

&lt;span class="c1"&gt;# GitLab CI — full history&lt;/span&gt;
&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;GIT_DEPTH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;⚠️ Shallow clones have a limitation you'll discover at exactly the wrong moment: if your build runs &lt;code&gt;git log&lt;/code&gt;, &lt;code&gt;git describe&lt;/code&gt;, or any comparison with older commits — for version number generation, changelog calculation, or comparing against a base commit in a PR — Git will tell you it doesn't have those commits. The fix is &lt;code&gt;fetch-depth: 0&lt;/code&gt;. Now you know this before learning it in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching: pre-staging the pieces
&lt;/h2&gt;

&lt;p&gt;A pipeline that takes four minutes and runs eighty times a day adds up to over five hours of distributed waiting across the team. Of those five hours, twenty minutes might be &lt;code&gt;npm install&lt;/code&gt; downloading &lt;code&gt;node_modules&lt;/code&gt; from scratch on every single run. With caching, that disappears.&lt;/p&gt;

&lt;p&gt;CI platforms have built-in caching systems that persist directories between runs — not the container itself, but specific folders you save and restore. The cache key typically includes a hash of the dependency lockfile, so it automatically invalidates when you change a dependency.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitHub Actions
&lt;/h3&gt;



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

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/cache@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;~/.npm&lt;/span&gt;
          &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}&lt;/span&gt;
          &lt;span class="na"&gt;restore-keys&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
            &lt;span class="s"&gt;${{ runner.os }}-node-&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install dependencies&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run tests&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;key&lt;/code&gt; is built from the hash of &lt;code&gt;package-lock.json&lt;/code&gt;. If the lockfile hasn't changed, the cache is exactly what the previous build left behind and &lt;code&gt;npm ci&lt;/code&gt; takes seconds instead of minutes. If it changed — new dependency, updated version — the cache invalidates and rebuilds.&lt;/p&gt;

&lt;h3&gt;
  
  
  GitLab CI
&lt;/h3&gt;



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

&lt;span class="na"&gt;variables&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;GIT_DEPTH&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1"&lt;/span&gt;

&lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;
  &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;node:20-slim&lt;/span&gt;
  &lt;span class="na"&gt;cache&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;$CI_COMMIT_REF_SLUG&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;node_modules/&lt;/span&gt;
  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm ci&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;npm test&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;GitLab CI uses &lt;code&gt;cache.key&lt;/code&gt; to identify the cache. &lt;code&gt;$CI_COMMIT_REF_SLUG&lt;/code&gt; is the branch name in a filesystem-safe format — each branch gets its own cache, which prevents one branch from corrupting another's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated tests on every commit
&lt;/h2&gt;

&lt;p&gt;The whole point of connecting Git to a pipeline is this: every commit gets verified automatically before it reaches main. The standard flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Developer pushes to a feature branch&lt;/li&gt;
&lt;li&gt;The pipeline clones, installs, and runs tests&lt;/li&gt;
&lt;li&gt;If tests fail, the PR/MR can't be merged&lt;/li&gt;
&lt;li&gt;If they pass, the code can be reviewed and merged with confidence
&lt;/li&gt;
&lt;/ol&gt;

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

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;test&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&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;npm ci&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;npm test&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;npm run lint&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Branch protection in GitHub (&lt;code&gt;Settings → Branches → Require status checks&lt;/code&gt;) makes the pipeline a mandatory condition for merging. Without green tests, the merge button is disabled. Simple in theory, career-saving in practice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automated deploy on merge
&lt;/h2&gt;

&lt;p&gt;The next step is connecting the merge to main with deployment. The most common pattern:&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;main&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Deploy to production&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;DEPLOY_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.DEPLOY_KEY }}&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;./scripts/deploy.sh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The deploy only runs on pushes to &lt;code&gt;main&lt;/code&gt; — not on PRs, not on feature branches. Secrets (&lt;code&gt;DEPLOY_KEY&lt;/code&gt;, cloud credentials, access tokens) live in the platform's secrets configuration and get injected as environment variables at runtime. Never in the repository. Never in the code. If you ever see a hardcoded key in a deploy script, that repository has a problem that isn't a CI problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Best practices for Git in CI
&lt;/h2&gt;

&lt;p&gt;Things most people learn the hard way:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use &lt;code&gt;fetch-depth: 0&lt;/code&gt; only when you need it.&lt;/strong&gt; Shallow clone is the right default. If a specific step needs full history, add &lt;code&gt;fetch-depth: 0&lt;/code&gt; to that job only, not everywhere.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;git fetch --tags&lt;/code&gt; if your build depends on tags.&lt;/strong&gt; Shallow clones don't fetch tags by default. If you use &lt;code&gt;git describe&lt;/code&gt; or generate version numbers from tags, you need an explicit fetch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;fetch-depth&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;
    &lt;span class="na"&gt;fetch-tags&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Minimum permissions for the token.&lt;/strong&gt; &lt;code&gt;GITHUB_TOKEN&lt;/code&gt; defaults to write permissions on the repository. If your workflow only reads, reduce them:&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;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Don't cache the &lt;code&gt;.git&lt;/code&gt; directory.&lt;/strong&gt; It might seem like a good idea to skip the clone entirely, but &lt;code&gt;.git&lt;/code&gt; changes on every run and the cache is almost never valid. What makes sense to cache is your dependencies: &lt;code&gt;node_modules&lt;/code&gt;, &lt;code&gt;.pip&lt;/code&gt;, &lt;code&gt;~/.gradle&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The pipeline that works locally and fails in CI has a cause.&lt;/strong&gt; Always. It might be an environment variable you assumed was present, a system dependency installed globally on your machine, or — if the runner is &lt;code&gt;windows-latest&lt;/code&gt; — mixed line endings because &lt;code&gt;core.autocrlf&lt;/code&gt; isn't consistently configured across the team (the exact setting from the Git configuration lesson). The runner doesn't lie: it runs exactly what you tell it. If it fails there and works locally, local has something implicit that isn't in the repository.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;In CI, each run starts from zero: no local configuration, no previous history, authentication injected by the platform.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;fetch-depth: 1&lt;/code&gt; (shallow clone) is the right default; use &lt;code&gt;fetch-depth: 0&lt;/code&gt; only when you need full history.&lt;/li&gt;
&lt;li&gt;Cache dependencies by lockfile hash — not the &lt;code&gt;.git&lt;/code&gt; directory.&lt;/li&gt;
&lt;li&gt;Secrets go in the platform's secrets configuration, never in the repository.&lt;/li&gt;
&lt;li&gt;Branch protection + required status checks = merges only happen when tests pass.&lt;/li&gt;
&lt;li&gt;If the pipeline fails and local works, local has something implicit you haven't declared.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Forty lessons. From &lt;code&gt;git init&lt;/code&gt; in an empty folder to signed commits, worktrees, automated hooks, LFS, submodules, and now pipelines that deploy code without anyone pressing a button. Git is the same tool it was in lesson one. What changed is the size of what you can do with it.&lt;/p&gt;

&lt;p&gt;A closing note on who makes it here:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Arch Linux users&lt;/strong&gt; probably had &lt;code&gt;delta&lt;/code&gt;, &lt;code&gt;rerere&lt;/code&gt;, and &lt;code&gt;fsmonitor&lt;/code&gt; configured before this course mentioned them, installed &lt;code&gt;lazygit&lt;/code&gt; from the AUR before we got to the productivity section, and have been running split panes in the terminal the entire time. They don't need the acknowledgment, but they've earned it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows users&lt;/strong&gt;: same ground covered, with the minor detail that somewhere in these forty lessons there was an unexpected &lt;code&gt;\r\n&lt;/code&gt; in the history, Git Bash and WSL2 coexist on the same machine, and the question of which one to use is technically still open. Spoiler: WSL2.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And those who made it here with VS Code, the Source Control panel, and a mouse&lt;/strong&gt;: Git is exactly the same underneath the buttons. You now know what happens when you click "Sync." That counts.&lt;/p&gt;

&lt;p&gt;What comes next — if the curiosity doesn't stop — is the infrastructure that gives context to all of this. The same &lt;code&gt;git push&lt;/code&gt; you've just learned to connect to a pipeline can end up updating a Kubernetes cluster, applying a Terraform plan, or notifying a monitoring system that a new version is live. That's exactly what the &lt;strong&gt;DevOps and Platform Engineering&lt;/strong&gt; course — coming soon — is about: Linux, networking, CI with Jenkins, Docker, Kubernetes, Terraform, observability, and everything between the code and the server. Git is the mother tool. Now you know how to use it.&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 switch and git restore: the end of the all-purpose checkout</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Fri, 16 Oct 2026 13:59:32 +0000</pubDate>
      <link>https://dev.to/fj_palacios/git-switch-and-git-restore-the-end-of-the-all-purpose-checkout-2m2i</link>
      <guid>https://dev.to/fj_palacios/git-switch-and-git-restore-the-end-of-the-all-purpose-checkout-2m2i</guid>
      <description>&lt;p&gt;&lt;code&gt;git checkout&lt;/code&gt; is Git's Swiss Army knife: five different tools folded on top of each other, and when you need the scissors, you pull out the corkscrew. It switches branches, creates new branches, restores files, restores files to a specific commit's version, and drops you into a detached HEAD state if you give it a hash. All valid. All the same command. None of them particularly obvious about what the others do.&lt;/p&gt;

&lt;p&gt;If you've ever felt that &lt;code&gt;git checkout&lt;/code&gt; was confusing — that it made no sense for the same command to handle branch navigation and file recovery — you were right. It wasn't just you. It was the design. Enough so that in 2019, with Git 2.23, two new commands arrived to split it apart: &lt;code&gt;git switch&lt;/code&gt; for branches, &lt;code&gt;git restore&lt;/code&gt; for files. They've been stable for years. This tutorial is about understanding them and making the switch.&lt;/p&gt;

&lt;h2&gt;
  
  
  git switch: changing branches without ambiguity
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;git switch&lt;/code&gt; does one thing: move HEAD to another branch. That's it. The syntax is straightforward:&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;# Switch to an existing branch&lt;/span&gt;
git switch main

&lt;span class="c"&gt;# Create a new branch and switch to it&lt;/span&gt;
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; feature/new-functionality

&lt;span class="c"&gt;# Create from a specific base&lt;/span&gt;
git switch &lt;span class="nt"&gt;-c&lt;/span&gt; feature/my-branch origin/develop

&lt;span class="c"&gt;# Return to the previous branch&lt;/span&gt;
git switch -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you're like me after years of muscle memory, &lt;code&gt;git checkout -b&lt;/code&gt; is wired into your left hand and &lt;code&gt;git switch -c&lt;/code&gt; will feel redundant for the first few weeks. That's normal. The difference isn't in what it does — it's in what it refuses to do: &lt;code&gt;git switch&lt;/code&gt; can't restore files, can't go to a specific commit, can't enter detached HEAD unless you explicitly ask for it. It switches branches. That's what makes it predictable.&lt;/p&gt;

&lt;p&gt;What happens if you try &lt;code&gt;git switch abc123&lt;/code&gt; to inspect a specific commit? Git doesn't guess: it gives you an error and tells you to use &lt;code&gt;--detach&lt;/code&gt; if that's what you want. You have to be intentional:&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;# You have to mean this&lt;/span&gt;
git switch &lt;span class="nt"&gt;--detach&lt;/span&gt; abc123
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;git checkout&lt;/code&gt;, ending up in a detached HEAD was too easy — you'd type a hash without thinking and suddenly be in a repository state that required explaining what HEAD was and why it was floating alone in space. With &lt;code&gt;git switch&lt;/code&gt;, that doesn't happen by accident.&lt;/p&gt;

&lt;h2&gt;
  
  
  git restore: recovering files without the mysterious double dash
&lt;/h2&gt;

&lt;p&gt;The classic way to discard changes in a file was &lt;code&gt;git checkout -- file.txt&lt;/code&gt;. The &lt;code&gt;--&lt;/code&gt; there comes from a Unix convention for separating flags from arguments — without it, Git can't tell if you're passing a branch name or a path. Technically necessary. Practically, one of those details you've spent years typing without knowing what that double dash is doing there, because nobody stopped to explain it and you already had it in your muscle memory before you thought to ask.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git restore&lt;/code&gt; makes it explicit:&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;# Discard working directory changes (restore to last committed version)&lt;/span&gt;
git restore file.txt

&lt;span class="c"&gt;# Restore multiple paths at once&lt;/span&gt;
git restore src/ tests/

&lt;span class="c"&gt;# Discard all tracked changes in working directory&lt;/span&gt;
git restore &lt;span class="nb"&gt;.&lt;/span&gt;

&lt;span class="c"&gt;# Restore a file to its state at a specific commit&lt;/span&gt;
git restore &lt;span class="nt"&gt;--source&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;abc123 file.txt

&lt;span class="c"&gt;# Unstage a file (move it back from index to working directory)&lt;/span&gt;
git restore &lt;span class="nt"&gt;--staged&lt;/span&gt; file.txt

&lt;span class="c"&gt;# Unstage and discard working directory changes in one shot&lt;/span&gt;
git restore &lt;span class="nt"&gt;--staged&lt;/span&gt; &lt;span class="nt"&gt;--worktree&lt;/span&gt; file.txt
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--staged&lt;/code&gt; flag is the one that makes the biggest difference day-to-day. Before, removing a file from the staging area meant &lt;code&gt;git reset HEAD file.txt&lt;/code&gt; — which works, but uses the word "reset" for something that isn't a reset, which caused that brief hesitation before hitting Enter. &lt;code&gt;git restore --staged&lt;/code&gt; does the same thing and says exactly what it does. No hesitation required.&lt;/p&gt;

&lt;p&gt;⚠️ &lt;code&gt;git restore&lt;/code&gt; without &lt;code&gt;--staged&lt;/code&gt; discards working directory changes permanently. They don't go to the reflog because they were never committed — they're just overwritten. If you're not sure, &lt;code&gt;git stash&lt;/code&gt; before &lt;code&gt;git restore&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The conversion table
&lt;/h2&gt;

&lt;p&gt;For when muscle memory still reaches for the old command:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Operation&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;Now&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Switch branch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git checkout main&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git switch main&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Create and switch&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git checkout -b new&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git switch -c new&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restore file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git checkout -- file.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git restore file.txt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Restore from commit&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git checkout abc123 -- file&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git restore --source=abc123 file&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unstage file&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git reset HEAD file.txt&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git restore --staged file.txt&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Detached HEAD&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git checkout abc123&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;git switch --detach abc123&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;git checkout&lt;/code&gt; isn't going anywhere — there's too much code, too many scripts, too much muscle memory in the world to remove it. But if you're onboarding someone new or starting a fresh project, &lt;code&gt;git switch&lt;/code&gt; and &lt;code&gt;git restore&lt;/code&gt; are the commands worth teaching first. Not because they're modern, but because each one does exactly one thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  git sparse-checkout: when you don't need the whole repository
&lt;/h2&gt;

&lt;p&gt;In a monorepo with fifty packages, you're usually working on two or three of them. Downloading the other forty-seven every time gives you nothing except slower clones and wasted disk space.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git sparse-checkout&lt;/code&gt; solves that: it lets you check out only the directories you need:&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;# Initialize sparse-checkout (cone mode is the standard)&lt;/span&gt;
git sparse-checkout init &lt;span class="nt"&gt;--cone&lt;/span&gt;

&lt;span class="c"&gt;# Specify which directories to include&lt;/span&gt;
git sparse-checkout &lt;span class="nb"&gt;set &lt;/span&gt;packages/my-app src/shared

&lt;span class="c"&gt;# Check what's currently configured&lt;/span&gt;
git sparse-checkout list

&lt;span class="c"&gt;# Add more directories without replacing the current set&lt;/span&gt;
git sparse-checkout add packages/another-lib

&lt;span class="c"&gt;# Disable and restore full checkout&lt;/span&gt;
git sparse-checkout disable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cone mode (Git 2.26) works with whole directories instead of arbitrary globs, which makes it significantly faster and more predictable. It's the mode you'll want to use in practice.&lt;/p&gt;

&lt;p&gt;Combined with &lt;code&gt;--filter&lt;/code&gt; at clone time, it goes even further:&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;# Only download metadata; file contents are fetched on demand&lt;/span&gt;
git clone &lt;span class="nt"&gt;--filter&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;blob:none &lt;span class="nt"&gt;--sparse&lt;/span&gt; https://github.com/org/monorepo
&lt;span class="nb"&gt;cd &lt;/span&gt;monorepo
git sparse-checkout &lt;span class="nb"&gt;set &lt;/span&gt;packages/my-app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;--filter=blob:none&lt;/code&gt;, Git downloads commits and trees but defers file contents until they're actually needed. In a monorepo with years of history, the difference between this and a full clone can be several minutes. In a CI pipeline that runs a hundred times a day, those minutes add up fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  git maintenance: the work Git does while you're not looking
&lt;/h2&gt;

&lt;p&gt;As a repository grows, Git's internal operations get slower: the commit graph gets recalculated from scratch, loose objects accumulate, the index grows. &lt;code&gt;git gc&lt;/code&gt; existed to clean all of that up, but it required either remembering to run it manually or trusting Git to trigger it automatically at the most inconvenient moment.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;git maintenance&lt;/code&gt; (Git 2.31) registers periodic background tasks with the operating system's scheduler to keep the repository healthy without interrupting 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;# Register this repo for background maintenance&lt;/span&gt;
git maintenance start

&lt;span class="c"&gt;# Run all maintenance tasks manually&lt;/span&gt;
git maintenance run

&lt;span class="c"&gt;# Stop background maintenance for this repo&lt;/span&gt;
git maintenance stop
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;git maintenance start&lt;/code&gt;, Git registers a task in cron (Linux/macOS) or Task Scheduler (Windows) that periodically runs four tasks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;commit-graph&lt;/strong&gt;: precomputes the commit graph so &lt;code&gt;git log&lt;/code&gt; and &lt;code&gt;git merge-base&lt;/code&gt; run faster.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;loose-objects&lt;/strong&gt;: packs loose objects accumulated since the last pack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;incremental-repack&lt;/strong&gt;: reorganizes object packs incrementally, without the cost of a full &lt;code&gt;git gc&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;prefetch&lt;/strong&gt;: downloads objects from the remote in the background so the next &lt;code&gt;git fetch&lt;/code&gt; is faster.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On a normal-sized repository, the effect is imperceptible. On a repository with years of history and tens of thousands of commits, it can make &lt;code&gt;git log&lt;/code&gt; and &lt;code&gt;git status&lt;/code&gt; noticeably faster without changing anything about how you work. Which is, essentially, the best version of any optimization.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;git switch&lt;/code&gt; replaces &lt;code&gt;git checkout&lt;/code&gt; for changing branches: it does only that, and detached HEAD requires explicit &lt;code&gt;--detach&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git restore&lt;/code&gt; replaces &lt;code&gt;git checkout -- file&lt;/code&gt; and &lt;code&gt;git reset HEAD file&lt;/code&gt;: &lt;code&gt;--staged&lt;/code&gt; moves files out of the index, without the flag it discards working directory changes.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git checkout&lt;/code&gt; will keep working, but &lt;code&gt;git switch&lt;/code&gt; and &lt;code&gt;git restore&lt;/code&gt; are what makes sense to use and teach in new projects.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git sparse-checkout --cone&lt;/code&gt; lets you check out only part of the repository: essential in monorepos or repos with large histories.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;git maintenance start&lt;/code&gt; registers background maintenance with no cost to your normal workflow.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;This is the second-to-last lesson in the course. In the final tutorial, we'll look at how Git fits into CI/CD pipelines: shallow clones so builds don't download the full commit history, object caching between runs, and the specific operations that make Git behave well when it's being run by an automated server instead of a developer at a terminal.&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>Prompts for Code Generation with AI</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Wed, 14 Oct 2026 10:13:56 +0000</pubDate>
      <link>https://dev.to/fj_palacios/prompts-for-code-generation-with-ai-30k0</link>
      <guid>https://dev.to/fj_palacios/prompts-for-code-generation-with-ai-30k0</guid>
      <description>&lt;p&gt;You type "write a function to manage users" and hit Enter.&lt;/p&gt;

&lt;p&gt;The AI comes back with 847 lines. An entity, a repository, a service, a controller, a DTO, a mapper, a validation decorator, and a README explaining the architecture. You wanted to filter by name.&lt;/p&gt;

&lt;p&gt;This is what happens when you ask for code without specifying what you actually want: the AI doesn't fill gaps with nothing — it fills them with competence. Impressive, thorough, and completely wrong for your use case.&lt;/p&gt;

&lt;p&gt;The problem isn't the AI. It's that "manage users" isn't a specification — it's an open invitation. And without constraints, AI tends to show everything it knows. Like that coworker who, when you ask what time it is, also explains how atomic clocks work.&lt;/p&gt;

&lt;p&gt;In the &lt;a href="https://dev.to/en/tutorials/using-ai-to-learn-programming-concepts"&gt;previous tutorial, you used AI as a learning tutor to understand programming concepts&lt;/a&gt;. Now the goal shifts: instead of asking for explanations, you're asking for code you can actually use. That requires a different way of asking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Specification first, code second
&lt;/h2&gt;

&lt;p&gt;The rule is straightforward: the better you describe what you want, the closer the result is to what you wanted.&lt;/p&gt;

&lt;p&gt;It sounds obvious. It is, until you see the difference in practice:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;❌ Vague specification:
"Write a function to process orders"

✅ Concrete specification:
"Write a Python function process_order() that:
- Accepts an Order object with fields: id, user_id, items (list
  of dicts with product_id and quantity), and status
- Calculates the total by summing price * quantity for each item
- Returns the Order object with total calculated and status set to 'processed'
- Raises ValueError if items is empty
- Raises OrderAlreadyProcessedError if status is already 'processed'
- Includes full type hints
- Uses no external dependencies"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why all the detail? Because "process orders" can mean forty different things depending on the system. The function generated from the vague prompt might work perfectly in a different context than yours.&lt;/p&gt;

&lt;p&gt;Isn't the AI supposed to be smart? It is. But smart isn't the same as psychic. Your most capable colleague would also ask three questions before touching the keyboard. The AI can't ask because you already told it to start.&lt;/p&gt;

&lt;p&gt;Elements that belong in a solid specification:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Name and signature&lt;/strong&gt; of the function or module&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What it receives&lt;/strong&gt; (types, input constraints)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What it returns&lt;/strong&gt; (return type, shape of the result)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error behavior&lt;/strong&gt; (which exceptions to raise, when)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Technical constraints&lt;/strong&gt; (language version, available libraries, project conventions)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What NOT to do&lt;/strong&gt; — sometimes more important than everything else&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last one deserves its own example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Implement without recursion.
Don't introduce external dependencies.
Don't use type syntax below Python 3.10.
Don't generate test code yet — just the function."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Negative constraints save time. Without them, the AI makes decisions that are reasonable from its perspective but may not be from yours. Then you spend ten minutes wondering why there's a &lt;code&gt;functools.reduce&lt;/code&gt; in a function you thought was simple.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scaffold approach: structure first, code second
&lt;/h2&gt;

&lt;p&gt;Asking for code directly is like building a house by starting with the curtains. The curtains look great. Then you discover the windows aren't standard size, the frames don't fit, and someone designed a bathroom with no door.&lt;/p&gt;

&lt;p&gt;The scaffold approach works the other way: structure first, then implement piece by piece.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prompt 1:
"Give me the complete file structure for a Flask REST API
for a todo app. Just the structure: filenames and one line
describing what each contains. No code yet."

Prompt 2:
"Now implement models.py with the models you described."

Prompt 3:
"Implement routes.py connecting to the models from the previous step."

Prompt 4:
"Write tests for routes.py."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each step is verifiable before moving forward. If the structure doesn't work for you, you fix it at prompt 1, not at prompt 4 when you already have 200 lines of code that depend on a flawed decision.&lt;/p&gt;

&lt;p&gt;There's a non-obvious benefit here too: breaking work into short steps gives the AI more room to reason. A specific, focused prompt consistently outperforms "do everything at once."&lt;/p&gt;

&lt;p&gt;A useful variation is a &lt;strong&gt;context block&lt;/strong&gt; — a chunk of project context you paste at the start of each important conversation. The AI doesn't remember previous sessions, but it does know everything in the current context. 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;"Project context:
- Django 5 backend with Django REST Framework
- PostgreSQL 15, Redis for caching, Celery for async tasks
- Python 3.12, strict typing with mypy
- All models follow snake_case, tests with pytest
- Domain separated into apps: orders/, products/, users/

With this context, implement..."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once per conversation, at the start. You don't need to repeat it every prompt.&lt;/p&gt;

&lt;h2&gt;
  
  
  TDD with AI: tests first
&lt;/h2&gt;

&lt;p&gt;The test-first approach with AI is, arguably, the most honest way to generate code. Not because it's more principled, but because it forces you to specify behavior before seeing the implementation.&lt;/p&gt;

&lt;p&gt;The logic: tests are easier to verify than code. You can read three lines of a test and know exactly what's expected. The 40 lines of implementation that make them pass are more opaque.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Prompt 1:
"Write failing pytest tests for a function add_user(name, email) that should:
- Accept a non-empty name and a valid email
- Raise ValueError if name is empty
- Raise ValueError if email has invalid format
- Return a dict with generated id, name, and email
Don't implement the function yet."

Prompt 2:
"Now implement add_user() to make these tests pass."

Prompt 3:
"What edge cases are missing from my tests?"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The third prompt is the most valuable step in the process. The AI typically surfaces scenarios you didn't consider: emails with Unicode characters, names with 500 characters, &lt;code&gt;None&lt;/code&gt; passed instead of a string, emails with leading spaces. Some will be irrelevant to your use case. Others will be the exact bug you'd have found in production two weeks later.&lt;/p&gt;

&lt;p&gt;If you're also working through the &lt;a href="https://dev.to/en/courses/learn-to-code-from-scratch-python"&gt;Python from scratch course&lt;/a&gt;, this pattern maps naturally to the functions lesson: define the signature, specify behavior through tests, let the implementation come after. It's a way to practice thinking in interfaces before thinking in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problems with generated code
&lt;/h2&gt;

&lt;p&gt;Generated code works. It compiles, sometimes has tests, the variable names are reasonable. What it doesn't have is context about what happened the last time someone tried something similar in production.&lt;/p&gt;

&lt;p&gt;You do.&lt;/p&gt;

&lt;p&gt;The most common problems that slip through unnoticed:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stale APIs.&lt;/strong&gt; The library changed eight months ago and the AI doesn't know. The generated code uses the previous version. It works in development because you have the old version pinned. It breaks on the production server because that one has the new version. The error shows up on a Friday afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hardcoded values.&lt;/strong&gt; Database credentials, API tokens, internal service URLs — all in the source code. Like hiding your house key under the welcome mat with a note that says "emergency key." Everybody knows about the welcome mat. So does your git history on GitHub.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Partial error handling.&lt;/strong&gt; The AI handles the cases you described. The ones you didn't mention don't exist to it. If you didn't say what should happen when the external service doesn't respond, there's a silent timeout waiting for its moment in production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Security issues.&lt;/strong&gt; SQL injection when inputs aren't sanitized correctly. Secrets leaking into logs. Validations that look complete but don't cover every execution path.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Performance that doesn't scale.&lt;/strong&gt; A query that works with 100 records and takes 45 seconds with 50,000. The N+1 that looks innocent until traffic grows and someone messages you at 3 AM.&lt;/p&gt;

&lt;p&gt;None of these are the AI's fault. They're a consequence of you having context it doesn't: the project's history, previous architectural decisions, bugs that already happened, the client's security requirements. That knowledge isn't transmitted in a prompt — it gets applied in review.&lt;/p&gt;

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

&lt;p&gt;Once you have generated code, this prompt catches most issues before they reach production:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"Review the code you just generated, specifically looking for:
1. Security vulnerabilities (injection, exposed secrets, input validation)
2. Performance issues (inefficient queries, unnecessary memory load)
3. Missing or incomplete error handling
4. Hardcoded values that should be configurable
5. Assumptions about the environment that might not hold

For each issue found: describe the problem, explain why it matters,
and propose the specific fix."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The AI runs through the same checks you'd do in a code review, before you do. And it sometimes catches things you'd have missed. Not always — especially problems that depend on your system's context. The review prompt reduces noise; your judgment as a developer is still the final filter.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;A concrete specification produces concrete code; a vague one produces architecture nobody asked for&lt;/li&gt;
&lt;li&gt;The scaffold approach (structure → models → routes → tests) avoids rewriting from the middle&lt;/li&gt;
&lt;li&gt;TDD with AI: write failing tests first, ask for an implementation that passes them, then ask what edge cases you missed&lt;/li&gt;
&lt;li&gt;Generated code works for the cases you specified — the ones you didn't mention are your responsibility&lt;/li&gt;
&lt;li&gt;The review prompt is a second pair of eyes before code review, not a replacement for it&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;There's a pattern in all of these approaches: the first response is rarely the final one. The scaffold takes multiple rounds, TDD has three prompts, the review adds another pass. That's not a problem with the process — it's how building code with AI actually works. In the next tutorial, we'll look at exactly that: how to manage the refinement dialogue without losing the thread or ending up with a function that no longer resembles what you asked for in the first place.&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>Advanced function parameters in Python: defaults, *args, and **kwargs</title>
      <dc:creator>Javi Palacios</dc:creator>
      <pubDate>Tue, 13 Oct 2026 11:06:36 +0000</pubDate>
      <link>https://dev.to/fj_palacios/advanced-function-parameters-in-python-defaults-args-and-kwargs-34nc</link>
      <guid>https://dev.to/fj_palacios/advanced-function-parameters-in-python-defaults-args-and-kwargs-34nc</guid>
      <description>&lt;p&gt;The function you wrote in the previous tutorial had one or two parameters. The one you'll write in six months will have eight. Someone on the team added four without telling anyone, one has a name nobody remembers the origin of, the seventh is a boolean called &lt;code&gt;flag&lt;/code&gt; whose behavior depends on the value of the parameter before it, and there's a &lt;code&gt;mode&lt;/code&gt; that "makes sense in context." This isn't a prediction; it's just how software grows.&lt;/p&gt;

&lt;p&gt;Python has tools to keep this from becoming a disaster: default parameters, keyword arguments, &lt;code&gt;*args&lt;/code&gt;, and &lt;code&gt;**kwargs&lt;/code&gt;. This lesson covers all of them — including why one of them has a trap that's caught more programmers than will admit it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Default parameters
&lt;/h2&gt;

&lt;p&gt;A default parameter has a value Python uses when you don't pass an argument for it:&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="n"&gt;greeting&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="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;greeting&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;greet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;               &lt;span class="c1"&gt;# Hello, Alice
&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;Bob&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Hey&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# Hey, Bob
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;greeting&lt;/code&gt; is optional. If you don't pass it, Python uses &lt;code&gt;"Hello"&lt;/code&gt;. If you do, Python uses what you send.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The rule&lt;/strong&gt;: default parameters always come after non-default ones. Python needs to know what's required and what's optional, and it figures that out by position.&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&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;discount&lt;/span&gt;&lt;span class="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="n"&gt;tax&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.21&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;   &lt;span class="c1"&gt;# ✅
&lt;/span&gt;    &lt;span class="n"&gt;base&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;discount&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;tax&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;discount&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="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="mf"&gt;0.21&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;   &lt;span class="c1"&gt;# ❌ SyntaxError
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you put the default parameter first, Python can't tell whether the first argument you pass is &lt;code&gt;discount&lt;/code&gt; or &lt;code&gt;price&lt;/code&gt;. The question makes sense; the answer is a syntax error.&lt;/p&gt;

&lt;h3&gt;
  
  
  The trap nobody warns you about
&lt;/h3&gt;

&lt;p&gt;Default values are evaluated &lt;strong&gt;once&lt;/strong&gt;, when Python defines the function — not each time you call it. For strings, numbers, and booleans that's fine because they're immutable. For lists and dictionaries, it matters a lot.&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_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt;
    &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;items&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_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;a&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# ['a']
&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_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;b&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# ['a', 'b']  ← wait
&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_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;c&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# ['a', 'b', 'c']  ← this wasn't the plan
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;[]&lt;/code&gt; in the definition is created once. Every call that doesn't pass &lt;code&gt;items&lt;/code&gt; shares the exact same object. You're accumulating items across calls even though it looks like you're starting fresh each time.&lt;/p&gt;

&lt;p&gt;Like a shared whiteboard in a meeting room: you walk in expecting a blank surface, but the diagrams from the previous meeting are still there. The whiteboard is the list; the diagrams are the elements from previous calls; and "I thought it started fresh" is wrong every time.&lt;/p&gt;

&lt;p&gt;The fix:&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_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;  &lt;span class="c1"&gt;# New list for each call without an explicit list
&lt;/span&gt;    &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&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;items&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rule: &lt;strong&gt;never use a mutable object as a default value&lt;/strong&gt; — list, dict, or set. Use &lt;code&gt;None&lt;/code&gt; and create the object inside the function. No exceptions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Positional and keyword arguments
&lt;/h2&gt;

&lt;p&gt;So far you've been passing arguments in order — those are &lt;strong&gt;positional&lt;/strong&gt; arguments. Python assigns them by position: first to the first parameter, second to the second, and so on.&lt;/p&gt;

&lt;p&gt;With two or three parameters, that's fine:&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="n"&gt;city&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;, &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;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;city&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="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;Alice&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;London&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;With six:&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;create_report&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pdf&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;full&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;What does that &lt;code&gt;True&lt;/code&gt; do? And the &lt;code&gt;False&lt;/code&gt;? Is &lt;code&gt;None&lt;/code&gt; intentional or did someone forget to pass something? You can't know without reading the function definition. And reading the function definition every time you call something isn't development — it's archaeology.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Keyword arguments&lt;/strong&gt; solve 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="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="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;London&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;Now each value carries its label. You can also change the order:&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;show_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;London&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And mix positional and keyword — always with positional first:&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;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;Alice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;London&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;age&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;34&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&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="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Alice&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;London&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="c1"&gt;# ❌ SyntaxError
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why bother? For the day your function has four boolean parameters and the person reading the code six months from now isn't you. &lt;code&gt;include_signature=True&lt;/code&gt; says what it does. &lt;code&gt;True&lt;/code&gt; in position four says nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  *args: variable number of positional arguments
&lt;/h2&gt;

&lt;p&gt;What if you need a function that accepts any number of arguments? The &lt;code&gt;*&lt;/code&gt; before the parameter name tells Python to pack all the extra positional arguments into a &lt;strong&gt;tuple&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&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;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;              &lt;span class="c1"&gt;# 3
&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;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&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="c1"&gt;# 15
&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="c1"&gt;# 0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Inside the function, &lt;code&gt;args&lt;/code&gt; is just a tuple with everything that arrived. The name &lt;code&gt;args&lt;/code&gt; is convention — the &lt;code&gt;*&lt;/code&gt; does the work. You could call it &lt;code&gt;*numbers&lt;/code&gt; or &lt;code&gt;*values&lt;/code&gt;. In practice, &lt;code&gt;*args&lt;/code&gt; is what everyone expects to see.&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;inspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&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;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;   &lt;span class="c1"&gt;# &amp;lt;class 'tuple'&amp;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;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;inspect&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;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hello&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# &amp;lt;class 'tuple'&amp;gt;
# (10, 20, 'hello')
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You can combine regular parameters with &lt;code&gt;*args&lt;/code&gt;, as long as &lt;code&gt;*args&lt;/code&gt; comes after the positional ones:&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_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;items&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;title&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&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;  - &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;item&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="nf"&gt;show_list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fruits&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;apple&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orange&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;banana&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;Fruits&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;apple&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;orange&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;banana&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Think of &lt;code&gt;*args&lt;/code&gt; like a function that takes however many pizza toppings you want — you don't hardcode three slots for &lt;code&gt;topping_1&lt;/code&gt;, &lt;code&gt;topping_2&lt;/code&gt;, &lt;code&gt;topping_3&lt;/code&gt;. Whatever shows up goes in.&lt;/p&gt;

&lt;h2&gt;
  
  
  **kwargs: variable keyword arguments
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;**kwargs&lt;/code&gt; does the same thing as &lt;code&gt;*args&lt;/code&gt;, but for keyword arguments. Instead of a tuple, Python packs everything into a &lt;strong&gt;dictionary&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;value&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="nf"&gt;configure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;blue&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;large&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;border&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&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 yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;color&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;blue&lt;/span&gt;
&lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;large&lt;/span&gt;
&lt;span class="na"&gt;border&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same as &lt;code&gt;*args&lt;/code&gt;, the name &lt;code&gt;kwargs&lt;/code&gt; is convention. The &lt;code&gt;**&lt;/code&gt; is what counts.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;**kwargs&lt;/code&gt; shows up a lot in code that builds or configures objects, in function wrappers, and in generic utilities that don't know in advance what options they'll receive.&lt;/p&gt;

&lt;p&gt;Honest warning: &lt;code&gt;**kwargs&lt;/code&gt; can make your function very flexible or completely opaque. If someone calls &lt;code&gt;configure(name="test", temperature=37, pizza=True)&lt;/code&gt; and the function accepts it without complaint, there's no way to know from the outside which parameters are actually valid. Use it when the flexibility makes real sense; not as a substitute for thinking through your function's interface. The warning sign: if you're calling &lt;code&gt;kwargs.get()&lt;/code&gt; in eight different places inside the same function, that function is probably doing too many things.&lt;/p&gt;

&lt;h2&gt;
  
  
  Combining everything
&lt;/h2&gt;

&lt;p&gt;You can combine regular parameters, &lt;code&gt;*args&lt;/code&gt;, and &lt;code&gt;**kwargs&lt;/code&gt; in one function. The order is always:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Regular positional parameters&lt;/li&gt;
&lt;li&gt;&lt;code&gt;*args&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Keyword-only parameters (come after &lt;code&gt;*args&lt;/code&gt; and can &lt;strong&gt;only&lt;/strong&gt; be passed by name)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;**kwargs&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;extra&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;[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;] timestamp=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;timestamp&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;messages&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;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;msg&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;extra&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;  extra: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;extra&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="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INFO&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Connection established&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Database ready&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;admin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;192.168.1.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[INFO] timestamp=False
  Connection established
  Database ready
  extra: {'user': 'admin', 'ip': '192.168.1.1'}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;timestamp&lt;/code&gt; isn't a regular positional parameter or part of &lt;code&gt;**kwargs&lt;/code&gt; — it sits between &lt;code&gt;*args&lt;/code&gt; and &lt;code&gt;**kwargs&lt;/code&gt;, which makes it &lt;strong&gt;keyword-only&lt;/strong&gt;: it can only be passed by name, never by position.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to use what
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Default parameters&lt;/strong&gt;: when a reasonable value works for most cases. Don't put a default on everything "just in case" — a function where everything is optional has no clear contract.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyword arguments&lt;/strong&gt;: when the signature has more than three parameters or when the argument types don't make their purpose obvious.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;*args&lt;/strong&gt;: when you genuinely don't know how many positional arguments will arrive. If you know, list them explicitly — an explicit signature always communicates more than &lt;code&gt;*args&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;*&lt;strong&gt;*kwargs&lt;/strong&gt;: for wrappers, decorators, and generic configuration utilities. Not as a substitute for thinking through the interface.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;ul&gt;
&lt;li&gt;Default parameters always go after required ones.&lt;/li&gt;
&lt;li&gt;Default values are evaluated once: &lt;strong&gt;never use a mutable object as a default value&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;Keyword arguments make code more readable when functions have many parameters.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;*args&lt;/code&gt; packs positional arguments into a tuple; &lt;code&gt;**kwargs&lt;/code&gt; packs keyword arguments into a dictionary.&lt;/li&gt;
&lt;li&gt;Parameters after &lt;code&gt;*args&lt;/code&gt; are keyword-only: they cannot be passed by position.&lt;/li&gt;
&lt;li&gt;The order is: positional → &lt;code&gt;*args&lt;/code&gt; → keyword-only → &lt;code&gt;**kwargs&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;💡 Challenge&lt;/strong&gt;: Create a function &lt;code&gt;log(message, level="INFO", *tags, timestamp=True, **extra)&lt;/code&gt;. Call it in several ways: just with &lt;code&gt;message&lt;/code&gt;, adding &lt;code&gt;level&lt;/code&gt;, passing extra tags, with &lt;code&gt;timestamp=False&lt;/code&gt;, and with extra keyword arguments like &lt;code&gt;user="admin"&lt;/code&gt;. What does it print each time? What happens if you try to pass &lt;code&gt;timestamp&lt;/code&gt; by position?&lt;/p&gt;

&lt;p&gt;Next up: &lt;strong&gt;scope&lt;/strong&gt; — how Python decides which variable is visible from where, what happens when a function defines a variable with the same name as one outside it, and why &lt;code&gt;global&lt;/code&gt; exists even though most of the time you're better off not using it.&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>
  </channel>
</rss>
