<?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: IriAndr</title>
    <description>The latest articles on DEV Community by IriAndr (@iriandr).</description>
    <link>https://dev.to/iriandr</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%2F4024450%2Ffcf425a7-e8ab-46cd-a587-e321517ecea3.png</url>
      <title>DEV Community: IriAndr</title>
      <link>https://dev.to/iriandr</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iriandr"/>
    <language>en</language>
    <item>
      <title>A 500 Error Taught Me More Than a Green Checkmark Would Have</title>
      <dc:creator>IriAndr</dc:creator>
      <pubDate>Mon, 20 Jul 2026 23:15:57 +0000</pubDate>
      <link>https://dev.to/iriandr/a-500-error-taught-me-more-than-a-green-checkmark-would-have-3040</link>
      <guid>https://dev.to/iriandr/a-500-error-taught-me-more-than-a-green-checkmark-would-have-3040</guid>
      <description>&lt;p&gt;I set up my first CI pipeline today. It failed on the first real run —&lt;br&gt;
not because of a typo, but because the pipeline wasn't actually testing&lt;br&gt;
what my app needed. Figuring out why turned into a short debugging&lt;br&gt;
exercise that was more useful than if everything had just passed.&lt;/p&gt;
&lt;h2&gt;
  
  
  What CI/CD actually means
&lt;/h2&gt;

&lt;p&gt;The idea is simple: instead of manually checking "does this still work?"&lt;br&gt;
every time you push code, you let a server do it automatically.&lt;/p&gt;

&lt;p&gt;CI (Continuous Integration) is that automatic check — build the project,&lt;br&gt;
run it, confirm nothing broke. CD (Continuous Deployment) goes a step&lt;br&gt;
further and automatically ships the working version somewhere. This&lt;br&gt;
project only covers CI: making sure the app builds and responds correctly&lt;br&gt;
on every push to &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  The workflow file
&lt;/h2&gt;

&lt;p&gt;GitHub looks for automation instructions in one specific place:&lt;br&gt;
&lt;code&gt;.github/workflows/&lt;/code&gt;. Any YAML file there gets picked up automatically.&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;Docker Build&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;build&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;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout code&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;Start services with docker compose&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;docker compose up -d --build&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;Wait for app to start&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;sleep &lt;/span&gt;&lt;span class="m"&gt;5&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;Check app responds&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;curl --fail http://localhost:5000&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;Show logs on failure&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;failure()&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;docker compose logs&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;Stop services&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;docker compose down&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A quick walk through what happens on every push:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;on: push: branches: [main]&lt;/code&gt; — this runs automatically whenever
something is pushed to &lt;code&gt;main&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;runs-on: ubuntu-latest&lt;/code&gt; — GitHub spins up a fresh, temporary Ubuntu
machine just for this job.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Checkout code&lt;/code&gt; pulls the repository onto that machine — without it,
there'd be nothing to build.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker compose up -d --build&lt;/code&gt; starts the whole stack, rebuilding the
image fresh each time.&lt;/li&gt;
&lt;li&gt;A short &lt;code&gt;sleep&lt;/code&gt; gives the app time to actually start listening before
anything tries to reach it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;curl --fail&lt;/code&gt; is the actual test — if the app doesn't respond correctly,
this step fails and the whole pipeline turns red.&lt;/li&gt;
&lt;li&gt;The logs step only runs &lt;code&gt;if: failure()&lt;/code&gt; — it stays silent when
everything's fine, and prints container logs when something isn't.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;docker compose down&lt;/code&gt; cleans up either way.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The first version — and why it failed
&lt;/h2&gt;

&lt;p&gt;My first attempt at this workflow didn't use &lt;code&gt;docker compose&lt;/code&gt; at all —&lt;br&gt;
I just reused the command from before I'd added Redis to the project:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run container&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;docker run -d -p 5000:5000 --name test-container hello-flask&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It ran. It failed. The error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;curl: (22) The requested URL returned error: 500
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A 500 is a specific kind of failure — it means the server responded, but&lt;br&gt;
something broke while handling the request. That ruled out a timing&lt;br&gt;
issue right away; if the app hadn't started yet, curl would have said&lt;br&gt;
"connection refused," not returned an actual HTTP error.&lt;/p&gt;

&lt;p&gt;Looking at &lt;code&gt;app.py&lt;/code&gt; narrowed it down further. There's exactly one thing&lt;br&gt;
in that file that can throw an unhandled error: the call to Redis.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;incr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hits&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;And that's when it clicked — the app had been updated to require Redis,&lt;br&gt;
but the CI workflow was still starting the app on its own, the way it&lt;br&gt;
used to run before Redis was part of the picture. The container had&lt;br&gt;
nothing to talk to.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix — and what it says about testing
&lt;/h2&gt;

&lt;p&gt;The fix was straightforward once the cause was clear: stop starting the&lt;br&gt;
app in isolation, and start the whole stack the same way it actually&lt;br&gt;
runs.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Start services with docker compose&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;docker compose up -d --build&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This single line replaced the manual &lt;code&gt;docker run&lt;/code&gt; step. Instead of&lt;br&gt;
starting one container in isolation, it reads &lt;code&gt;docker-compose.yml&lt;/code&gt; and&lt;br&gt;
brings up everything the app actually depends on — in this case, both&lt;br&gt;
&lt;code&gt;web&lt;/code&gt; and &lt;code&gt;redis&lt;/code&gt;. Once Redis was actually there to answer, the app&lt;br&gt;
stopped failing.&lt;/p&gt;

&lt;p&gt;The bigger lesson wasn't really about Docker or YAML syntax. It was that&lt;br&gt;
a CI check is only useful if it tests the thing you actually run. My&lt;br&gt;
first version passed a build check, but it wasn't testing my app — it&lt;br&gt;
was testing an older version of it that no longer existed in the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually mattered
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;A 500 error and a connection failure mean different things — the error
code itself is a clue&lt;/li&gt;
&lt;li&gt;CI configuration can quietly drift out of sync with the actual app,
especially when you copy an old command instead of updating it&lt;/li&gt;
&lt;li&gt;Adding a logging step for failures (&lt;code&gt;if: failure()&lt;/code&gt;) cost nothing to
write and will save time the next time something breaks&lt;/li&gt;
&lt;li&gt;A pipeline that always passes isn't necessarily testing anything
meaningful — this one only became useful once it could actually fail
for the right reason&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>devops</category>
      <category>github</category>
      <category>cicd</category>
      <category>docker</category>
    </item>
    <item>
      <title>Containers, Compose, and a Counter That Actually Persists</title>
      <dc:creator>IriAndr</dc:creator>
      <pubDate>Mon, 13 Jul 2026 11:22:37 +0000</pubDate>
      <link>https://dev.to/iriandr/containers-compose-and-a-counter-that-actually-persists-434b</link>
      <guid>https://dev.to/iriandr/containers-compose-and-a-counter-that-actually-persists-434b</guid>
      <description>&lt;p&gt;The moment Docker clicked for me wasn't when I ran &lt;code&gt;hello-world&lt;/code&gt;. It was&lt;br&gt;
when I understood what a container actually &lt;em&gt;is&lt;/em&gt;: not just "your app, but&lt;br&gt;
packaged" — more like a whole room, built exactly to spec, with everything&lt;br&gt;
inside it already arranged. The walls, the furniture, and the app itself&lt;br&gt;
are all part of the same package. You're not just shipping code, you're&lt;br&gt;
shipping the room it lives in.&lt;/p&gt;

&lt;p&gt;That idea became a lot more concrete once I built something that needed&lt;br&gt;
two rooms talking to each other.&lt;/p&gt;
&lt;h2&gt;
  
  
  Building the room — the Dockerfile
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;Dockerfile&lt;/code&gt; is the blueprint for that room — a step-by-step recipe&lt;br&gt;
that says: start with this foundation, add these tools, put your code&lt;br&gt;
here, and here's how to turn the lights on.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; python:3.12-slim&lt;/span&gt;

&lt;span class="k"&gt;WORKDIR&lt;/span&gt;&lt;span class="s"&gt; /app&lt;/span&gt;

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; requirements.txt .&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt

&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; app.py .&lt;/span&gt;

&lt;span class="k"&gt;EXPOSE&lt;/span&gt;&lt;span class="s"&gt; 5000&lt;/span&gt;

&lt;span class="k"&gt;CMD&lt;/span&gt;&lt;span class="s"&gt; ["python", "app.py"]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;FROM python:3.12-slim&lt;/code&gt; sets the foundation — a lightweight image that
already has Python installed, so I don't have to build an operating
system from scratch.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;WORKDIR /app&lt;/code&gt; sets where everything happens inside the room — every
command after this runs from that folder.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;COPY requirements.txt .&lt;/code&gt; and &lt;code&gt;RUN pip install -r requirements.txt&lt;/code&gt;
happen &lt;em&gt;before&lt;/em&gt; copying the actual app code. That ordering matters:
Docker caches each step, so if I only change my app code later, it
won't reinstall every dependency from scratch — only the steps after
the change get rebuilt.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;EXPOSE 5000&lt;/code&gt; documents which port the app listens on inside the
container. It doesn't open anything by itself — that happens later,
when the container actually runs.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;CMD&lt;/code&gt; is the last step: what to run the moment the room is occupied —
in this case, start the Flask server.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Building the image from this blueprint is one command:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Running a container from it is another:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 5000:5000 hello-flask
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-p 5000:5000&lt;/code&gt; part maps a port on my machine to a port inside the&lt;br&gt;
container — without it, the app would be running, but there'd be no way&lt;br&gt;
to reach it from outside.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two rooms, one hallway — docker-compose
&lt;/h2&gt;

&lt;p&gt;One container is simple. But real applications rarely live alone — mine&lt;br&gt;
needed a place to store data that survives beyond a single request. I&lt;br&gt;
added Redis, a key-value store, to keep a visit counter that wouldn't&lt;br&gt;
reset every time the app restarted.&lt;/p&gt;

&lt;p&gt;This meant two containers now had to run together and talk to each other.&lt;br&gt;
That's what &lt;code&gt;docker-compose.yml&lt;/code&gt; is 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;web&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;.&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;5000:5000"&lt;/span&gt;
    &lt;span class="na"&gt;depends_on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;redis&lt;/span&gt;

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

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;build: .&lt;/code&gt; tells Compose to build the &lt;code&gt;web&lt;/code&gt; service from my Dockerfile,&lt;br&gt;
the same way I did manually before. &lt;code&gt;image: redis:alpine&lt;/code&gt; does the&lt;br&gt;
opposite — instead of building anything, it just pulls Redis's official,&lt;br&gt;
lightweight image.&lt;/p&gt;

&lt;p&gt;The part that actually made everything click was realizing how the two&lt;br&gt;
containers find each other. It's not through the &lt;code&gt;ports&lt;/code&gt; section — that&lt;br&gt;
only maps my machine to the &lt;code&gt;web&lt;/code&gt; container, and has nothing to do with&lt;br&gt;
&lt;code&gt;web&lt;/code&gt; talking to &lt;code&gt;redis&lt;/code&gt;. The real connection happens because Compose&lt;br&gt;
automatically creates a shared network between all the services in the&lt;br&gt;
file, and inside that network, each container can be reached by its&lt;br&gt;
service name.&lt;/p&gt;

&lt;p&gt;That's why the app code just does this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;redis&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Redis&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;redis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;port&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6379&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;"redis"&lt;/code&gt; isn't a placeholder — it's the literal name of the service&lt;br&gt;
from the compose file. No IP addresses, no manual network configuration.&lt;br&gt;
Docker handles the discovery on its own.&lt;/p&gt;

&lt;p&gt;With both files in place, starting everything is one command:&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;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Compose reads the file, starts &lt;code&gt;redis&lt;/code&gt; first (because of &lt;code&gt;depends_on&lt;/code&gt;),&lt;br&gt;
then builds and starts &lt;code&gt;web&lt;/code&gt;. Refreshing the page a few times confirmed&lt;br&gt;
it was actually working:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Hello from Docker! This page has been viewed 8 times.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The count kept climbing with every refresh — proof that the state was&lt;br&gt;
living in Redis, not disappearing every time the page reloaded.&lt;/p&gt;
&lt;h2&gt;
  
  
  An unexpected git detour
&lt;/h2&gt;

&lt;p&gt;Pushing this project to GitHub hit a snag that had nothing to do with&lt;br&gt;
Docker. I'd created the GitHub repository with an auto-generated README,&lt;br&gt;
then tried to push my local commits on top of it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;!&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;rejected]        main -&amp;gt; main &lt;span class="o"&gt;(&lt;/span&gt;fetch first&lt;span class="o"&gt;)&lt;/span&gt;
error: failed to push some refs to &lt;span class="s1"&gt;'...'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git was refusing to overwrite history it didn't recognize — my local&lt;br&gt;
repo and the GitHub repo had each started with their own independent&lt;br&gt;
first commit, so there was nothing connecting them yet. The fix was to&lt;br&gt;
pull the remote history in and let Git combine the two:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git pull origin main &lt;span class="nt"&gt;--allow-unrelated-histories&lt;/span&gt; &lt;span class="nt"&gt;--no-rebase&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That flag exists specifically for this situation — Git normally refuses&lt;br&gt;
to merge two histories with no common ancestor, as a safety measure&lt;br&gt;
against accidentally combining unrelated projects. Since I knew exactly&lt;br&gt;
why these two histories had diverged, it was safe to allow it. No&lt;br&gt;
conflicts came up this time, just a merge commit to confirm, and the&lt;br&gt;
push went through right after.&lt;/p&gt;

&lt;p&gt;Small reminder that most of the real learning in this stuff doesn't&lt;br&gt;
come from the parts that go smoothly.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Before this, Docker felt like a black box — something everyone in DevOps&lt;br&gt;
uses, but hard to picture without actually building something with it.&lt;br&gt;
Writing a Dockerfile from scratch and connecting two containers through&lt;br&gt;
Compose made it concrete.&lt;/p&gt;

&lt;p&gt;A few things stuck with me:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The order of steps in a Dockerfile matters, not just for correctness
but for how efficiently images rebuild&lt;/li&gt;
&lt;li&gt;Containers in the same Compose file find each other by service name,
not by ports or IP addresses&lt;/li&gt;
&lt;li&gt;Not every problem along the way is about the tool you're learning —
sometimes it's just Git, doing what Git does&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>beginners</category>
      <category>docker</category>
      <category>python</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>My First Git Merge Conflict (On Purpose)</title>
      <dc:creator>IriAndr</dc:creator>
      <pubDate>Sun, 12 Jul 2026 11:51:31 +0000</pubDate>
      <link>https://dev.to/iriandr/my-first-git-merge-conflict-on-purpose-17m0</link>
      <guid>https://dev.to/iriandr/my-first-git-merge-conflict-on-purpose-17m0</guid>
      <description>&lt;p&gt;Today I did something I'd only read about before: I created a Git merge&lt;br&gt;
conflict on purpose, and then resolved it by hand. I wanted to understand&lt;br&gt;
exactly what happens when two branches change the same line differently —&lt;br&gt;
not just memorize the commands, but actually see the process.&lt;/p&gt;
&lt;h2&gt;
  
  
  Branches basics
&lt;/h2&gt;

&lt;p&gt;A branch in Git is just a separate line of work. It lets you make changes&lt;br&gt;
without touching the main codebase until you're ready to merge them back.&lt;/p&gt;

&lt;p&gt;To create a new branch and switch to it in one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; feature/add-greeting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;-b&lt;/code&gt; flag tells Git to create the branch first, then switch to it.&lt;br&gt;
You can confirm which branch you're on with:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;The current branch is marked with an asterisk.&lt;/p&gt;

&lt;p&gt;One thing that confused me at first: why &lt;code&gt;checkout&lt;/code&gt;? It sounds like&lt;br&gt;
something you'd do at a library, not something related to branches.&lt;br&gt;
Turns out that's exactly the idea — &lt;code&gt;checkout&lt;/code&gt; comes from a "check out&lt;br&gt;
a book" metaphor. You're telling Git "give me this version of the files."&lt;br&gt;
Git even introduced a newer, clearer command for this — &lt;code&gt;git switch&lt;/code&gt; —&lt;br&gt;
because &lt;code&gt;checkout&lt;/code&gt; historically does too many different things and&lt;br&gt;
confuses beginners. But &lt;code&gt;checkout -b&lt;/code&gt; is still what most tutorials and&lt;br&gt;
teams use, so it's worth knowing first.&lt;/p&gt;
&lt;h2&gt;
  
  
  First merge — the easy one
&lt;/h2&gt;

&lt;p&gt;Once my branch had a commit, I switched back to &lt;code&gt;main&lt;/code&gt; and merged it in:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout main
git merge feature/add-greeting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I expected this to be more dramatic. Instead, Git just said "Fast-forward"&lt;br&gt;
and moved on. No conflict, no drama.&lt;/p&gt;

&lt;p&gt;Here's why: a conflict only happens when two branches diverge from the&lt;br&gt;
same point and change the same content independently. My feature branch&lt;br&gt;
was just sitting on top of main with nothing new happening on main in the&lt;br&gt;
meantime — so Git could simply "fast-forward" main to match the branch,&lt;br&gt;
no merging required.&lt;/p&gt;

&lt;p&gt;That's when I realized: to actually see a conflict, I'd need to make main&lt;br&gt;
and my branch disagree about something, on purpose.&lt;/p&gt;
&lt;h2&gt;
  
  
  Creating a real conflict on purpose
&lt;/h2&gt;

&lt;p&gt;My first attempt to force a conflict didn't work either. I edited&lt;br&gt;
&lt;code&gt;greeting.txt&lt;/code&gt; on &lt;code&gt;main&lt;/code&gt;, committed it, then created a new branch and&lt;br&gt;
edited the same file there too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; feature/change-greeting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Still no conflict — another fast-forward. The reason: I'd created this&lt;br&gt;
branch &lt;em&gt;after&lt;/em&gt; the change on main, so there was still no real divergence,&lt;br&gt;
just a straight line of commits.&lt;/p&gt;

&lt;p&gt;To actually diverge, my branch needed to start from a point &lt;em&gt;before&lt;/em&gt; the&lt;br&gt;
latest change on main. I checked the commit history:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;and created a new branch from an earlier commit, not the latest one:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; feature/real-conflict a0ae0d3
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Hello from a truly diverging feature!"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; greeting.txt
git add greeting.txt
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Diverging change from feature"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now &lt;code&gt;main&lt;/code&gt; and &lt;code&gt;feature/real-conflict&lt;/code&gt; had both changed the same line of&lt;br&gt;
&lt;code&gt;greeting.txt&lt;/code&gt;, independently, from the same starting point. Merging them&lt;br&gt;
finally produced what I was looking for:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout main
git merge feature/real-conflict
&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;CONFLICT (content): Merge conflict in greeting.txt
Automatic merge failed; fix conflicts and then commit the result.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Resolving the conflict
&lt;/h2&gt;

&lt;p&gt;Opening the conflicted file showed something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD
&lt;/span&gt;&lt;span class="p"&gt;Hello from main - second edit!
&lt;/span&gt;&lt;span class="gh"&gt;=======
&lt;/span&gt;&lt;span class="p"&gt;Hello from a truly diverging feature!
&lt;/span&gt;&lt;span class="gi"&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; feature/real-conflict
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Git can't decide which version is "correct" — both are valid changes, made&lt;br&gt;
independently — so it inserts both versions directly into the file,&lt;br&gt;
wrapped in markers, and leaves the decision to you.&lt;/p&gt;

&lt;p&gt;Here's what each marker means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt;&amp;lt; HEAD&lt;/code&gt; — start of the version on your current branch&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;=======&lt;/code&gt; — separator between the two versions&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt;&amp;gt; feature/real-conflict&lt;/code&gt; — end of the version from the branch
you're merging in&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The markers themselves aren't part of your code — they're temporary, and&lt;br&gt;
Git expects you to remove them before committing. I opened the file in&lt;br&gt;
&lt;code&gt;nano&lt;/code&gt; (a simpler alternative to &lt;code&gt;vim&lt;/code&gt; for anyone just starting out),&lt;br&gt;
deleted the marker lines, decided what the final content should be, and&lt;br&gt;
saved.&lt;/p&gt;

&lt;p&gt;Then it's just a normal 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 greeting.txt
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Resolve merge conflict in greeting.txt"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Running &lt;code&gt;git log --oneline --graph&lt;/code&gt; afterward made the whole story visible&lt;br&gt;
at once — the branches splitting apart and merging back together at the&lt;br&gt;
conflict resolution commit.&lt;/p&gt;
&lt;h2&gt;
  
  
  Pull Request workflow
&lt;/h2&gt;

&lt;p&gt;Resolving conflicts locally is one thing, but in real teams, changes&lt;br&gt;
usually go through a Pull Request first — a place for review before&lt;br&gt;
anything reaches &lt;code&gt;main&lt;/code&gt;. I wanted to go through that flow too, even&lt;br&gt;
working solo.&lt;/p&gt;

&lt;p&gt;After cleaning up my practice branches, I created a fresh one for an&lt;br&gt;
actual feature:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git checkout &lt;span class="nt"&gt;-b&lt;/span&gt; feature/add-notes
&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"This repo is my Git practice playground."&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; notes.md
git add notes.md
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Add notes about this repo"&lt;/span&gt;
git push origin feature/add-notes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pushing for the first time meant dealing with authentication — GitHub&lt;br&gt;
no longer accepts a regular account password for Git operations. Instead,&lt;br&gt;
you need a Personal Access Token, generated from GitHub's developer&lt;br&gt;
settings and used in place of a password. Once I set&lt;br&gt;
&lt;code&gt;credential.helper store&lt;/code&gt;, Git remembered it for future pushes.&lt;/p&gt;

&lt;p&gt;The push output included a direct link to open a Pull Request. From&lt;br&gt;
there, the GitHub interface shows exactly what a reviewer would see:&lt;br&gt;
a diff of changed files, commit history, and a merge button — in my case,&lt;br&gt;
with "No conflicts with base branch," since this change didn't overlap&lt;br&gt;
with anything else.&lt;/p&gt;

&lt;p&gt;Clicking "Merge pull request" combined the branch into &lt;code&gt;main&lt;/code&gt;, and&lt;br&gt;
GitHub offered to delete the now-unused branch. Back in the terminal,&lt;br&gt;
one command brought my local copy up to date:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I learned
&lt;/h2&gt;

&lt;p&gt;Before today, "merge conflict" was one of those phrases that sounded&lt;br&gt;
scarier than it turned out to be. Once I saw what it actually looks&lt;br&gt;
like — two versions of the same line, wrapped in plain markers, waiting&lt;br&gt;
for a decision — it stopped being abstract.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Conflicts only happen when branches genuinely diverge, not just because
you're working on separate branches&lt;/li&gt;
&lt;li&gt;The conflict markers are just text — nothing to be careful around,
just something to read and clean up&lt;/li&gt;
&lt;li&gt;The PR workflow exists for a reason: even solo, going through it made
the whole process feel less like typing commands and more like actually
working the way teams do&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>beginners</category>
      <category>github</category>
      <category>git</category>
      <category>devops</category>
    </item>
    <item>
      <title>Why I'm Learning DevOps in Public</title>
      <dc:creator>IriAndr</dc:creator>
      <pubDate>Fri, 10 Jul 2026 19:35:03 +0000</pubDate>
      <link>https://dev.to/iriandr/why-im-learning-devops-in-public-4e4d</link>
      <guid>https://dev.to/iriandr/why-im-learning-devops-in-public-4e4d</guid>
      <description>&lt;p&gt;I just installed WSL2, set up Ubuntu, and ran my first Docker container. &lt;br&gt;
Nothing groundbreaking — but it's the start of something I want to document &lt;br&gt;
properly, so here we are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why DevOps
&lt;/h2&gt;

&lt;p&gt;I want to work as a DevOps engineer, ideally starting with an internship. &lt;br&gt;
Right now I'm at the very beginning: comfortable with a computer, but new to &lt;br&gt;
Linux, containers, and most of the tooling this field runs on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why write about it
&lt;/h2&gt;

&lt;p&gt;Two reasons. First, writing forces me to actually understand what I did, &lt;br&gt;
not just copy-paste commands that happened to work. Second, English isn't &lt;br&gt;
my first language, and technical writing is a good way to practice it &lt;br&gt;
while doing something useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to expect here
&lt;/h2&gt;

&lt;p&gt;I'll be posting as I go through each stage:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Linux and the terminal&lt;/li&gt;
&lt;li&gt;Git and GitHub workflows&lt;/li&gt;
&lt;li&gt;Docker, from basics to docker-compose&lt;/li&gt;
&lt;li&gt;CI/CD with GitHub Actions&lt;/li&gt;
&lt;li&gt;Kubernetes basics&lt;/li&gt;
&lt;li&gt;Infrastructure as Code with Terraform&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some posts will be short notes, others will be closer to tutorials, written &lt;br&gt;
the way I wish someone had explained it to me.&lt;/p&gt;

&lt;p&gt;If you're on a similar path, feel free to follow along.&lt;/p&gt;

</description>
      <category>beginners</category>
      <category>devops</category>
      <category>learning</category>
      <category>linux</category>
    </item>
  </channel>
</rss>
