<?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: Tesleem Amuda</title>
    <description>The latest articles on DEV Community by Tesleem Amuda (@tesddev).</description>
    <link>https://dev.to/tesddev</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%2F1150610%2Fdb0af98e-37cd-44de-a7ae-def1a0793ee5.jpeg</url>
      <title>DEV Community: Tesleem Amuda</title>
      <link>https://dev.to/tesddev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tesddev"/>
    <language>en</language>
    <item>
      <title>The Docker Mistakes That Only Show Up After You Deploy</title>
      <dc:creator>Tesleem Amuda</dc:creator>
      <pubDate>Thu, 27 Aug 2026 00:17:45 +0000</pubDate>
      <link>https://dev.to/tesddev/the-docker-mistakes-that-only-show-up-after-you-deploy-bgb</link>
      <guid>https://dev.to/tesddev/the-docker-mistakes-that-only-show-up-after-you-deploy-bgb</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpbu0jdhlnsplm4g464w6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpbu0jdhlnsplm4g464w6.jpg" alt="Docker" width="800" height="537"&gt;&lt;/a&gt;Most Docker tutorials stop at docker build and call it done. The real problems, the ones that actually cost you time, show up later. When you push to a registry. When CI tries to reproduce your laptop build. When the chip on your machine is not the chip on the server.&lt;/p&gt;

&lt;p&gt;This is what broke while I was taking a FastAPI service from a working local Compose stack to something I would let CI ship. And why each fix works the way it does.&lt;/p&gt;

&lt;p&gt;## Multi-stage builds are not optional once the image leaves your laptop&lt;/p&gt;

&lt;p&gt;A single-stage Dockerfile that does pip install and COPY . . will build. It will also hand you a 400MB+ image for an API with three routes. Pip's cache, build tools, and the whole project directory end up inside a production artifact that never needed any of it.&lt;/p&gt;

&lt;p&gt;The fix is structural, not clever. Build in one stage. Run in another. Copy across only the finished product.&lt;/p&gt;

&lt;p&gt;My builder stage creates a virtualenv and installs dependencies into it. The runtime stage starts from a fresh copy of the same base image and copies exactly two things: that venv, and the application code. No compilers. No .git. No pip cache sitting in a layer you will pay to pull forever.&lt;/p&gt;

&lt;p&gt;Final size: 170MB. The gap between that and a naive build is not a rounding error. It is the difference between an image your runners pull in two seconds and one that makes every pipeline feel slower than it should.&lt;/p&gt;

&lt;p&gt;Do this every time as well: run as a non-root user inside the container.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RUN adduser --disabled-password --gecos '' appuser  
USER appuser
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  &lt;em&gt;depends_on&lt;/em&gt; does not mean what people think it means
&lt;/h3&gt;

&lt;p&gt;Compose depends_on waits for a container to &lt;em&gt;start&lt;/em&gt;. It says nothing about whether that container can do any work yet.&lt;/p&gt;

&lt;p&gt;Postgres is the usual trap. The container process is up in under a second. The database is not ready to accept connections for a few seconds after that. Your app starts, tries to connect immediately, fails, then either crash-loops or burns retries depending on how you wrote the client.&lt;/p&gt;

&lt;p&gt;The fix is depends_on with condition: service_healthy, paired with a real healthcheck. For Postgres, pg_isready is the right check.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;services:
  db:
    image: postgres:16-alpine
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app"]
      interval: 5s
      timeout: 5s
      retries: 10
  api:
    depends_on:
      db:
        condition: service_healthy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the API waits until Postgres can answer a query, not until PID 1 exists.&lt;/p&gt;

&lt;p&gt;This bug almost never shows up in a demo. Demos are slow enough, by accident, that the race does not fire. It shows up in CI, under load, or at 2am. Fix it before it has a ticket number.&lt;/p&gt;

&lt;h3&gt;
  
  
  The architecture mismatch nobody puts in the quickstart
&lt;/h3&gt;

&lt;p&gt;Build on Apple Silicon, deploy to a standard cloud VM, and you will hit this. Your image is arm64. The server is amd64. Docker will refuse to run it, or worse, emulate it badly enough that the container crash-loops and looks like an application bug.&lt;/p&gt;

&lt;p&gt;The signature I keep seeing: container stuck in Restarting (255), logs that do not mention the app at all. The failure is under the process you think you are debugging.&lt;/p&gt;

&lt;p&gt;Build both architectures and push a manifest list, not a single flat image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker buildx build \
  --platform linux/amd64,linux/arm64 \
  --tag ghcr.io/you/fastapi-service:latest \
  --push .
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The registry then serves the variant that matches the host pulling it. One tag. Correct binary on every machine. You stop discovering architecture mismatches after the deploy already ran.&lt;/p&gt;

&lt;p&gt;If you only need the server's architecture, still be explicit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;docker buildx build --platform linux/amd64 --push .

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

&lt;/div&gt;



&lt;p&gt;Implicit "whatever my laptop is" is how this class of bug is born.&lt;/p&gt;

&lt;h3&gt;
  
  
  CI that blocks, not CI that narrates
&lt;/h3&gt;

&lt;p&gt;A pipeline that runs lint, test, and build, then paints green checkmarks, is only half the job. The other half is proving those checks can stop a merge.&lt;/p&gt;

&lt;p&gt;Split the work into separate jobs and make build depend on test. That part is easy. The part worth verifying is the failure path. Break an assertion on purpose. Push it. Watch test go red and build get skipped because its dependency never passed.&lt;/p&gt;

&lt;p&gt;Then turn on branch protection with required status checks. That is what turns a red X into a greyed-out merge button. A pipeline that reports failures without enforcing them is just a slower way to learn that something is broken.&lt;/p&gt;

&lt;p&gt;If you have not watched a bad commit get blocked, you do not know whether the pipeline works. You only know that it runs.&lt;/p&gt;

&lt;h3&gt;
  
  
  The permission error that is not about your YAML
&lt;/h3&gt;

&lt;p&gt;Deploying from GitHub Actions to GHCR, you will eventually hit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;denied: permission_denied: write_package

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

&lt;/div&gt;



&lt;p&gt;You already scoped the workflow token. packages: write is in the file. The job still dies.&lt;/p&gt;

&lt;p&gt;The cause is easy to miss. If the package was first pushed from a laptop with a personal token, the package does not automatically trust the repo's GITHUB_TOKEN. The workflow can ask for write. The package can still say no.&lt;/p&gt;

&lt;p&gt;The fix is in the package settings, not in YAML. Open the package, go to &lt;strong&gt;Manage Actions access&lt;/strong&gt;, link the repository, grant write. One-time change. A permissions: block cannot override it, because the restriction lives on the package, not in the workflow.&lt;/p&gt;

&lt;p&gt;Same class of problem as the others. The error shows up in Actions. The lever is one screen over.&lt;/p&gt;

&lt;h3&gt;
  
  
  What actually matters
&lt;/h3&gt;

&lt;p&gt;None of this is exotic. It is the gap between "I can write a Dockerfile" and "I can let a machine ship this."&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Multi-stage builds are not a nice-to-have. They are how you stop shipping a workshop as a runtime.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Health checks are not paranoia. They are how you stop a startup race from becoming an outage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cross-platform builds are not an edge case if you develop on Apple Silicon and deploy to amd64, which is a normal setup now.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;CI that cannot block a merge is monitoring, not control.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Registry permissions live on the package. The workflow file is not the whole story.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern across all of it: the failure never shows up where you were looking. It shows up one layer down, after the part you tested already worked.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Swap `ghcr.io/you/fastapi-service` for your real image name before you publish.   ``
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



</description>
    </item>
    <item>
      <title>Your API Isn't Ready for the Internet Yet</title>
      <dc:creator>Tesleem Amuda</dc:creator>
      <pubDate>Sat, 08 Aug 2026 21:13:07 +0000</pubDate>
      <link>https://dev.to/tesddev/your-api-isnt-ready-for-the-internet-yet-1ofn</link>
      <guid>https://dev.to/tesddev/your-api-isnt-ready-for-the-internet-yet-1ofn</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fna5bwjyq0au4pqtmxx3a.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fna5bwjyq0au4pqtmxx3a.jpg" alt="Reverse Proxy Nginx" width="800" height="537"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most people deploy their first API the same way: expose it on &lt;code&gt;0.0.0.0&lt;/code&gt; and hope for the best.&lt;br&gt;
That's a mistake.&lt;br&gt;
A production-ready API should never face the internet directly. Here's how to set it up properly, and why each layer exists.&lt;/p&gt;
&lt;h3&gt;
  
  
  Keep the API private by design
&lt;/h3&gt;

&lt;p&gt;When your application starts, the bind address is a security decision.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0.0.0.0&lt;/code&gt; means "accept connections from anywhere." Anyone who discovers the port can talk straight to your process.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;127.0.0.1&lt;/code&gt; means "accept connections only from this machine." Nothing outside the server can reach it.
Always bind to &lt;code&gt;127.0.0.1&lt;/code&gt;.
Your application was never meant to be the public face of the service. Something else should handle that job.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Put a reverse proxy in front
&lt;/h3&gt;

&lt;p&gt;A reverse proxy sits between the internet and your application. Every request arrives at the proxy first. The proxy decides what to do, then forwards the request only when appropriate.&lt;br&gt;
With Nginx the flow is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Browser → Nginx (HTTPS, port 443) → Your API (HTTP, 127.0.0.1:3000)

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

&lt;/div&gt;



&lt;p&gt;Nginx is the only process the outside world ever touches. Your API works behind a locked door. Nginx holds the only key.&lt;br&gt;
This separation delivers several practical benefits:&lt;br&gt;
Nginx terminates TLS. Your application speaks plain HTTP and never needs to know about certificates. Encryption lives in one place instead of being duplicated across every service.&lt;br&gt;
Nginx can log, rate-limit, and route without changing your code. Adding another service later is just another location block.&lt;br&gt;
If the application crashes, the failure stays private. A raw stack trace never reaches the browser. Nginx can return a controlled response instead.&lt;br&gt;
A minimal location block 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;location /api/ {
 proxy_pass http://127.0.0.1:3000/;
 proxy_set_header Host $host;
 proxy_set_header X-Real-IP $remote_addr;
 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
 proxy_set_header X-Forwarded-Proto $scheme;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;proxy_pass&lt;/code&gt; line does the forwarding. The header lines are essential. Without them, every request appears to come from Nginx itself and your application loses the real client information.&lt;br&gt;
Verify both directions. Confirm the API responds correctly through the public domain. Then confirm it does &lt;em&gt;not&lt;/em&gt; respond when you hit the server IP directly on the application port. A working reverse proxy is not only "the front door works." It is also "there is no back door."&lt;br&gt;
Let systemd keep the process alive&lt;br&gt;
Starting an application in a terminal is fine for development. It is not acceptable in production. Close the session and the process dies.&lt;br&gt;
systemd exists to start, stop, and supervise long-running services. A minimal service file looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[Service]
User=deploy
WorkingDirectory=/home/deploy/projects/api
ExecStart=/home/deploy/projects/api/venv/bin/uvicorn main:app - host 127.0.0.1 - port 3000
Restart=always
RestartSec=5
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Restart=always&lt;/code&gt; combined with &lt;code&gt;RestartSec=5&lt;/code&gt; is the key.&lt;br&gt;
In plain English: if the process ever stops for any reason, wait five seconds and start it again. No human intervention required.&lt;br&gt;
Test it the hard way. Send a &lt;code&gt;kill -9&lt;/code&gt; to the process. Within seconds a new process appears with a fresh PID. Reboot the server. When the machine comes back, the service is already running because it was enabled on boot.&lt;br&gt;
That is the real value of &lt;code&gt;Restart=always&lt;/code&gt;. The system recovers while you are offline.&lt;/p&gt;

&lt;h3&gt;
  
  
  Make the deployment idempotent
&lt;/h3&gt;

&lt;p&gt;The final piece is a single script that installs dependencies, deploys the code, configures systemd, and configures Nginx. The script must be safe to run more than once.&lt;br&gt;
Run it on a fresh server and it builds everything from scratch. Run it again on the same server and nothing breaks or duplicates. That property is called idempotency.&lt;br&gt;
A deploy script you cannot safely re-run is a script you will eventually be afraid to use. Design for the second run from the beginning.&lt;/p&gt;

&lt;h3&gt;
  
  
  Practical rules that hold up
&lt;/h3&gt;

&lt;p&gt;Bind the application to &lt;code&gt;127.0.0.1&lt;/code&gt;. Let Nginx be the only process the internet can reach. Hand the responsibility of staying alive to systemd. Write every deployment step so it can be repeated safely.&lt;br&gt;
These are not advanced techniques. They are the baseline difference between a service that works for a demo and one that remains correct months later with no one watching it.&lt;/p&gt;

&lt;p&gt;Full example: &lt;a href="https://github.com/tesddev/fastapi-nginx-service" rel="noopener noreferrer"&gt;github.com/tesddev/fastapi-nginx-service&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Understanding Subjects in RxSwift: The Day Everything Clicked</title>
      <dc:creator>Tesleem Amuda</dc:creator>
      <pubDate>Thu, 06 Aug 2026 13:32:00 +0000</pubDate>
      <link>https://dev.to/tesddev/understanding-subjects-in-rxswift-the-day-everything-clicked-5b9m</link>
      <guid>https://dev.to/tesddev/understanding-subjects-in-rxswift-the-day-everything-clicked-5b9m</guid>
      <description>&lt;p&gt;I remember the exact moment Subjects stopped feeling like magic and started feeling like tools I could actually control.&lt;/p&gt;

&lt;p&gt;I had been using RxSwift for a while. Observables made sense. Operators were powerful. Binding worked. But every time I needed to push values into a stream myself — from a network callback, a button action, or some internal state change — I just reached for PublishSubject and moved on. It worked… until it didn’t.&lt;/p&gt;

&lt;p&gt;Then one day I sat down and actually looked at the different types of Subjects. That was the turning point. Suddenly a lot of the weird behaviour I’d been fighting made sense, and I started writing cleaner reactive code.&lt;/p&gt;

&lt;p&gt;This article is the deep-dive I wish I had back then.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quick Refresher: What Even Is a Subject?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A Subject is both an Observable and an Observer.&lt;/p&gt;

&lt;p&gt;That means:&lt;/p&gt;

&lt;p&gt;You can subscribe to it (like a normal Observable)&lt;/p&gt;

&lt;p&gt;You can also call .onNext(), .onError(), and .onCompleted() on it yourself&lt;/p&gt;

&lt;p&gt;This dual nature is extremely useful, but it’s also where a lot of the confusion (and bugs) come from.&lt;/p&gt;

&lt;p&gt;There are four main types in RxSwift:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PublishSubject&lt;/li&gt;
&lt;li&gt; BehaviorSubject&lt;/li&gt;
&lt;li&gt; ReplaySubject&lt;/li&gt;
&lt;li&gt;AsyncSubject&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let’s go through each one properly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. PublishSubject – The Event Bus&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;publishSubject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;PublishSubject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;publishSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// Nobody is listening yet → gone forever&lt;/span&gt;

&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;subscription&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;publishSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;onNext&lt;/span&gt;&lt;span class="p"&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="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;publishSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// prints "B"&lt;/span&gt;

&lt;span class="n"&gt;publishSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"C"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// prints "C"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key behaviour:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Starts empty&lt;/li&gt;
&lt;li&gt; Only emits values to subscribers after they subscribe&lt;/li&gt;
&lt;li&gt; Does not keep any history&lt;/li&gt;
&lt;li&gt; Perfect for discrete events (button taps, notifications, “something just happened”)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When I reach for it:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;UI events that shouldn’t replay&lt;/li&gt;
&lt;li&gt; One-shot signals between layers&lt;/li&gt;
&lt;li&gt; Any time I truly don’t care about past values&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common pitfall:&lt;/strong&gt;&lt;br&gt;
People sometimes use PublishSubject for state. Then a late subscriber gets nothing and the UI stays blank. That’s usually a sign you needed a BehaviorSubject instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. BehaviorSubject – State That Always Has a Current Value&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;behaviorSubject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s"&gt;"Initial"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;behaviorSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;onNext&lt;/span&gt;&lt;span class="p"&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="s"&gt;"Subscriber 1:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Immediately prints: Subscriber 1: Initial&lt;/span&gt;

&lt;span class="n"&gt;behaviorSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Updated"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;behaviorSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;onNext&lt;/span&gt;&lt;span class="p"&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="s"&gt;"Subscriber 2:"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// Immediately prints: Subscriber 2: Updated&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key behaviour:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Requires an initial value&lt;/li&gt;
&lt;li&gt; Always holds the latest value&lt;/li&gt;
&lt;li&gt; New subscribers get that latest value immediately&lt;/li&gt;
&lt;li&gt; After that, they get new values as they arrive&lt;/li&gt;
&lt;li&gt;This is the Subject I use the most in real apps.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Typical use cases:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Current user profile&lt;/li&gt;
&lt;li&gt;Selected tab or filter&lt;/li&gt;
&lt;li&gt;Loading / error / success state of a screen&lt;/li&gt;
&lt;li&gt;Any piece of state that the UI needs right now&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Important notes:&lt;/strong&gt;&lt;br&gt;
Calling .onCompleted() or .onError() terminates it permanently. After that, new subscribers only get the terminal event.&lt;/p&gt;

&lt;p&gt;If you need something that never completes and is easier to work with from the UI side, many people prefer BehaviorRelay (from RxRelay). But under the hood it’s still built on BehaviorSubject.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common pitfall:&lt;/strong&gt;&lt;br&gt;
Creating a BehaviorSubject with a dummy initial value just to satisfy the compiler, then immediately overwriting it. That dummy value often leaks into the UI for a brief moment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. ReplaySubject – “Here’s What You Missed”&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;replaySubject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;ReplaySubject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;bufferSize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;replaySubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;replaySubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;replaySubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"C"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;replaySubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;onNext&lt;/span&gt;&lt;span class="p"&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="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// prints:&lt;/span&gt;

&lt;span class="c1"&gt;// B&lt;/span&gt;

&lt;span class="c1"&gt;// C&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key behaviour:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Keeps a buffer of the last N values&lt;/li&gt;
&lt;li&gt;New subscribers receive those buffered values first, then live values&lt;/li&gt;
&lt;li&gt;You can also create an unbounded version with .createUnbounded(), but be careful with memory&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;When it’s useful:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You need a short history (last few locations, last few search results, etc.)&lt;/li&gt;
&lt;li&gt;Multiple subscribers joining at different times should all see recent activity&lt;/li&gt;
&lt;li&gt;Debugging or logging streams where context matters&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Common pitfall:&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
Using a large or unbounded buffer without thinking. It’s easy to accidentally keep a lot of objects alive and create memory pressure.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. AsyncSubject – The Final Result Only&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;asyncSubject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;AsyncSubject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;String&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;asyncSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;onNext&lt;/span&gt;&lt;span class="p"&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="nv"&gt;$0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;asyncSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"A"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"B"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"C"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;asyncSubject&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onCompleted&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;// Only now does it print: C&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Key behaviour:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ignores everything until the sequence completes&lt;/li&gt;
&lt;li&gt;Then emits only the last value (if any) and completes&lt;/li&gt;
&lt;li&gt;If it errors, subscribers get the error instead&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This one is the least used in day-to-day iOS work, but it’s perfect for “I only care about the final answer” scenarios — like a long-running calculation or a multi-step process that should only notify when fully done.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Common pitfall:&lt;/strong&gt;  &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forgetting to call .onCompleted(). Without it, nothing ever emits.&lt;/li&gt;
&lt;li&gt;Side-by-Side Comparison&lt;/li&gt;
&lt;/ul&gt;

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

&lt;h2&gt;
  
  
  Real-World Pitfalls I’ve Hit (and How to Avoid Them)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Subjects and Memory Leaks&lt;/strong&gt;&lt;br&gt;
Subjects themselves don’t cause retain cycles, but the way we hold them does.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;ViewModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;PublishSubject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;Void&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;



    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;setup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="n"&gt;events&lt;/span&gt;

            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subscribe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;onNext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="k"&gt;weak&lt;/span&gt; &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt;   &lt;span class="c1"&gt;// ← easy to forget&lt;/span&gt;

                &lt;span class="k"&gt;self&lt;/span&gt;&lt;span class="p"&gt;?&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

            &lt;span class="p"&gt;})&lt;/span&gt;

            &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;disposed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;by&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;disposeBag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Always capture self weakly inside the subscription if the subject lives on self.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Completing a Subject Too Early&lt;/strong&gt;&lt;br&gt;
Once a Subject receives .onCompleted() or .onError(), it’s done. Forever. New subscribers only get the terminal event.&lt;/p&gt;

&lt;p&gt;If you need a long-lived stream of events, never complete the Subject unless you’re intentionally shutting it down.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Using the Wrong Subject for State&lt;/strong&gt;&lt;br&gt;
This is the most common intermediate mistake.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Need the current value right away when someone subscribes? → BehaviorSubject&lt;/li&gt;
&lt;li&gt;Pure event that shouldn’t replay? → PublishSubject&lt;/li&gt;
&lt;li&gt;Need a few recent values? → ReplaySubject&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Choosing wrong usually shows up as “the second screen that opens doesn’t have the data” or “old events keep firing when they shouldn’t.”&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Threading Surprises&lt;/strong&gt;&lt;br&gt;
Subjects are not magically thread-safe in the way some people expect. If you call .onNext from multiple threads without care, you can get race conditions.&lt;/p&gt;

&lt;p&gt;In practice I usually make sure all emissions happen on a known scheduler (often MainScheduler for UI-related subjects).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Over-using Subjects&lt;/strong&gt;&lt;br&gt;
Subjects are convenient, but they make your code more imperative. Whenever possible, prefer pure Observables that are created from existing sources (network, notifications, UI controls, etc.). Reach for a Subject only when you genuinely need to push values from outside the reactive chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  A Practical Pattern I Use Often
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="kd"&gt;final&lt;/span&gt; &lt;span class="kd"&gt;class&lt;/span&gt; &lt;span class="kt"&gt;SomeViewModel&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="c1"&gt;// Private so only the ViewModel can emit&lt;/span&gt;

    &lt;span class="kd"&gt;private&lt;/span&gt; &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="nv"&gt;_state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kt"&gt;BehaviorSubject&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;ViewState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loading&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;



    &lt;span class="c1"&gt;// Public read-only version&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="nv"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;Observable&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kt"&gt;ViewState&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="n"&gt;_state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;asObservable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="kd"&gt;func&lt;/span&gt; &lt;span class="nf"&gt;loadData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

        &lt;span class="c1"&gt;// ... network call&lt;/span&gt;

        &lt;span class="n"&gt;_state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loaded&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="c1"&gt;// or&lt;/span&gt;

        &lt;span class="n"&gt;_state&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;onNext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This pattern keeps the mutability private while still giving the UI a clean Observable to bind to.&lt;/p&gt;

&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;The day I properly understood the difference between PublishSubject, BehaviorSubject, ReplaySubject and AsyncSubject was the day my RxSwift code got noticeably cleaner. I stopped fighting the library and started choosing the right tool for the job.&lt;/p&gt;

&lt;p&gt;You don’t need to memorise every edge case. Just remember the core idea:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Publish → pure events  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Behavior → current state  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Replay → recent history  &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Async → final result only  &lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Once that clicks, the rest becomes muscle memory.&lt;/p&gt;

&lt;p&gt;If you’ve been using mostly PublishSubject for everything (like I did for a long time), try rewriting one of your ViewModels with the more appropriate Subject types. You’ll feel the difference quickly.&lt;/p&gt;

</description>
      <category>ios</category>
      <category>rxswift</category>
      <category>uikit</category>
      <category>swiftui</category>
    </item>
    <item>
      <title>How I Automated Ubuntu Server Hardening with One Script</title>
      <dc:creator>Tesleem Amuda</dc:creator>
      <pubDate>Fri, 31 Jul 2026 00:51:54 +0000</pubDate>
      <link>https://dev.to/tesddev/how-i-automated-ubuntu-server-hardening-with-one-script-209a</link>
      <guid>https://dev.to/tesddev/how-i-automated-ubuntu-server-hardening-with-one-script-209a</guid>
      <description>&lt;h1&gt;
  
  
  Securing and Automating a Cloud Server
&lt;/h1&gt;

&lt;p&gt;The first time I spun up a cloud server, I thought the hard part was over. It wasn't. The server was open to the world: root login enabled, port 22 broadcasting itself to every scanner on the internet, no firewall, and no certificate.&lt;/p&gt;

&lt;p&gt;I checked &lt;code&gt;/var/log/auth.log&lt;/code&gt; a few hours later and found hundreds of failed login attempts. That was the moment I understood why SSH hardening isn't optional.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why SSH Hardening Matters
&lt;/h2&gt;

&lt;p&gt;SSH is the front door to your server. By default, that door is wide open: port 22, password authentication allowed, and root login permitted. Automated bots scan millions of IP addresses every hour looking for exactly this.&lt;/p&gt;

&lt;p&gt;Hardening SSH means closing that gap before anything else touches the server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disable root login: &lt;code&gt;PermitRootLogin no&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Disable password authentication and use keys only: &lt;code&gt;PasswordAuthentication no&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Move SSH off port 22—I use port &lt;code&gt;2247&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Restrict login to one user: &lt;code&gt;AllowUsers deploy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Disconnect idle sessions after 10 minutes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;ClientAliveInterval 300&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;ClientAliveCountMax 2&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These five changes eliminate the vast majority of automated attacks. You're not invisible, but you're no longer an easy target.&lt;/p&gt;

&lt;h2&gt;
  
  
  What UFW Does
&lt;/h2&gt;

&lt;p&gt;UFW—Uncomplicated Firewall—is Ubuntu's approachable interface to &lt;code&gt;iptables&lt;/code&gt;. Rather than writing low-level rules, you express intent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ufw default deny incoming
ufw default allow outgoing

ufw allow 2247/tcp   &lt;span class="c"&gt;# SSH on custom port&lt;/span&gt;
ufw allow 80/tcp     &lt;span class="c"&gt;# HTTP&lt;/span&gt;
ufw allow 443/tcp    &lt;span class="c"&gt;# HTTPS&lt;/span&gt;

ufw &lt;span class="nt"&gt;--force&lt;/span&gt; &lt;span class="nb"&gt;enable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Everything not explicitly allowed is blocked. That's the principle of least privilege applied to networking, and it's the same principle that governs good Kubernetes &lt;code&gt;NetworkPolicies&lt;/code&gt;, IAM roles, and RBAC.&lt;/p&gt;

&lt;p&gt;Start learning it here.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Let's Encrypt Works
&lt;/h2&gt;

&lt;p&gt;Let's Encrypt is a free, automated Certificate Authority trusted by major browsers. The flow is straightforward:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;You point a domain at your server's IP address.&lt;/li&gt;
&lt;li&gt;Certbot serves a challenge file over HTTP to prove that you control the domain.&lt;/li&gt;
&lt;li&gt;Let's Encrypt issues a certificate valid for 90 days.&lt;/li&gt;
&lt;li&gt;Certbot rewrites your Nginx configuration to serve HTTPS and redirect HTTP automatically.&lt;/li&gt;
&lt;li&gt;A scheduled task renews the certificate before it expires—no manual work needed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result is a free, automatically maintained HTTPS website.&lt;/p&gt;

&lt;p&gt;The following command confirms that the renewal pipeline works without modifying your live certificate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;certbot renew &lt;span class="nt"&gt;--dry-run&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What Idempotency Means—and Why It Matters
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Idempotent&lt;/strong&gt; means that running the same operation twice produces the same result as running it once.&lt;/p&gt;

&lt;p&gt;It sounds academic. It isn't.&lt;/p&gt;

&lt;p&gt;In practice, it means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If &lt;code&gt;deploy&lt;/code&gt; already exists, skip it instead of returning an error.&lt;/li&gt;
&lt;li&gt;If the certificate already exists, skip it instead of requesting a duplicate.&lt;/li&gt;
&lt;li&gt;If packages are already installed, &lt;code&gt;apt-get&lt;/code&gt; handles them gracefully.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Without idempotency, rerunning a setup script after a partial failure could lock you out of your own server. With it, you can run the script confidently after any interruption.&lt;/p&gt;

&lt;p&gt;This habit carries forward into Ansible, Terraform, and every automation tool you'll use at a professional level.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Script
&lt;/h2&gt;

&lt;p&gt;The complete script is available here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/tesddev/server-bootstrap" rel="noopener noreferrer"&gt;github.com/tesddev/server-bootstrap&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On a fresh Ubuntu 22.04 server, one command does everything:&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;bash setup.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In order, the script:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Updates the system and installs essential packages.&lt;/li&gt;
&lt;li&gt;Creates a non-root &lt;code&gt;deploy&lt;/code&gt; user and copies in the SSH key.&lt;/li&gt;
&lt;li&gt;Hardens SSH by changing the port, disabling root login and password authentication, and setting an idle timeout.&lt;/li&gt;
&lt;li&gt;Configures UFW with an explicit allowlist.&lt;/li&gt;
&lt;li&gt;Installs Nginx and serves a custom page.&lt;/li&gt;
&lt;li&gt;Obtains a Let's Encrypt certificate using Certbot.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  What You Get at the End
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;SSH restricted to key authentication on a non-standard port&lt;/li&gt;
&lt;li&gt;A firewall permitting only necessary traffic&lt;/li&gt;
&lt;li&gt;A live website at &lt;a href="https://tes-devops.duckdns.org" rel="noopener noreferrer"&gt;https://tes-devops.duckdns.org&lt;/a&gt; with a valid, automatically renewing certificate&lt;/li&gt;
&lt;li&gt;A script you can run on any new server today or six months from now&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Real Lesson
&lt;/h2&gt;

&lt;p&gt;Manual server setup doesn't scale, and more importantly, it doesn't reproduce reliably. The same steps performed by two different people on two different days will produce two subtly different servers.&lt;/p&gt;

&lt;p&gt;Scripts don't have that problem.&lt;/p&gt;

&lt;p&gt;Writing small, idempotent automation scripts is one of the most transferable habits in DevOps. Everything that follows—Ansible playbooks, Terraform modules, and Kubernetes manifests—is this same idea applied at a larger scale.&lt;/p&gt;

&lt;p&gt;➡️ &lt;a href="https://github.com/tesddev/server-bootstrap" rel="noopener noreferrer"&gt;View the server-bootstrap repository on GitHub&lt;/a&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>ubuntu</category>
      <category>automation</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Automated User Management with Bash Script</title>
      <dc:creator>Tesleem Amuda</dc:creator>
      <pubDate>Mon, 01 Jul 2024 15:36:41 +0000</pubDate>
      <link>https://dev.to/tesddev/automated-user-management-with-bash-script-3b3e</link>
      <guid>https://dev.to/tesddev/automated-user-management-with-bash-script-3b3e</guid>
      <description>&lt;p&gt;Managing user accounts in a Linux environment can be tedious, especially when dealing with a large number of new employees. To simplify this process, we can use a Bash script to automate user and group creation, ensuring appropriate permissions and logging. Below is a detailed breakdown of a Bash script that accomplishes this.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;Script Overview&lt;/strong&gt;&lt;/h3&gt;&lt;br&gt;
The script reads a text file containing usernames and group names, creates users and groups as specified, sets up home directories with appropriate permissions and ownership, generates random passwords for the users, and logs all actions to /var/log/user_management.log. It also securely stores the generated passwords in /var/secure/user_passwords.txt.&lt;p&gt;&lt;/p&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;Script Breakdown&lt;/strong&gt;&lt;/h3&gt;&lt;br&gt;
&lt;p&gt;&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#!/bin/bash

# Check if the input file is provided
if [ $# -eq 0 ]; then
    echo "Usage: $0 &amp;lt;user_list_file&amp;gt;"
    exit 1
fi

user_list_file="$1"

# Log and password file paths
log_file="/var/log/user_management.log"
password_file="/var/secure/user_passwords.txt"

# Create the necessary directories and set permissions
mkdir -p /var/log
mkdir -p /var/secure
touch "$log_file"
touch "$password_file"
chmod 600 "$password_file"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Input File Check&lt;/strong&gt;: The script starts by ensuring that an input file is provided. This file should contain the list of users to be managed.&lt;br&gt;
&lt;strong&gt;Directory and File Setup&lt;/strong&gt;: It creates directories and files necessary for logging and storing passwords. Permissions are set to ensure security.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Function to log actions
log_action() {
    echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" &amp;gt;&amp;gt; "$log_file"
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Logging Function&lt;/strong&gt;: A function log_action is defined to log each action taken by the script. This helps in auditing and troubleshooting.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Read the user list file
while IFS=';' read -r username groups; do
    # Remove whitespace
    username=$(echo "$username" | xargs)
    groups=$(echo "$groups" | xargs)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Reading the User List&lt;/strong&gt;: The script reads each line from the input file, expecting a username and groups separated by a semicolon. Whitespace is trimmed to ensure clean data.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    # Ensure all specified groups exist
    for group in $(echo "$groups" | tr ',' ' '); do
        if ! getent group "$group" &amp;gt;/dev/null; then
            groupadd "$group"
            if [ $? -eq 0 ]; then
                log_action "Created group $group"
            else
                log_action "Failed to create group $group"
                continue
            fi
        else
            log_action "Group $group already exists"
        fi
    done
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Group Management&lt;/strong&gt;: The script checks if each specified group exists and creates it if it doesn't. Actions are logged accordingly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    # Create the personal group
    if ! getent group "$username" &amp;gt;/dev/null; then
        groupadd "$username"
        if [ $? -eq 0 ]; then
            log_action "Created group $username"
        else
            log_action "Failed to create group $username"
            continue
        fi
    else
        log_action "Group $username already exists"
    fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Personal Group Creation&lt;/strong&gt;: For each user, a personal group with the same name is created if it doesn't already exist.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    # Create the user with the personal group
    if ! id -u "$username" &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
        useradd -m -g "$username" -s /bin/bash "$username"
        if [ $? -eq 0 ]; then
            log_action "Created user $username with personal group $username"
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;User Creation&lt;/strong&gt;: If the user doesn't already exist, the script creates the user account, assigns the personal group, and sets the default shell to bash.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;            # Set the user's additional groups
            if [ -n "$groups" ]; then
                usermod -aG "$groups" "$username"
                if [ $? -eq 0 ]; then
                    log_action "Added user $username to groups $groups"
                else
                    log_action "Failed to add user $username to groups $groups"
                fi
            fi

            # Generate a random password
            password=$(openssl rand -base64 12)
            echo "$username:$password" | chpasswd
            if [ $? -eq 0 ]; then
                log_action "Set password for user $username"
            else
                log_action "Failed to set password for user $username"
            fi

            # Save the password securely
            echo "$username,$password" &amp;gt;&amp;gt; "$password_file"
        else
            log_action "Failed to create user $username"
        fi
    else
        log_action "User $username already exists"
    fi
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Additional Group Assignment&lt;/strong&gt;: If additional groups are specified, the user is added to these groups.&lt;br&gt;
&lt;strong&gt;Password Management&lt;/strong&gt;: A random password is generated and set for the user. The password is stored securely in a file with restricted permissions.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    # Set home directory permissions
    chmod 700 "/home/$username"
    chown "$username:$username" "/home/$username"
    log_action "Set permissions for /home/$username"
done &amp;lt; "$user_list_file"

log_action "Script execution completed."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Home Directory Permissions&lt;/strong&gt;: The script sets strict permissions on the user's home directory to ensure privacy and security.&lt;/p&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;Key Features&lt;/strong&gt;&lt;/h3&gt;&lt;p&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;u&gt;Group Creation&lt;/u&gt;: Ensures all specified groups exist before assigning users to them, preventing errors and ensuring proper group membership.&lt;/li&gt;
&lt;li&gt;
&lt;u&gt;User Creation&lt;/u&gt;: Creates users with personal groups and sets up their home directories with appropriate permissions.&lt;/li&gt;
&lt;li&gt;
&lt;u&gt;Password Generation&lt;/u&gt;: Generates random, secure passwords for new users and stores them securely.&lt;/li&gt;
&lt;li&gt;
&lt;u&gt;Logging&lt;/u&gt;: Logs all actions to a log file for audit purposes and troubleshooting.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;br&gt;&lt;br&gt;
&lt;strong&gt;&lt;/strong&gt;&lt;/p&gt;&lt;h3&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/h3&gt;&lt;br&gt;
This script is a robust solution for managing user accounts in a Linux environment. By automating the creation and management of users and groups, it saves time and reduces the potential for errors.&lt;p&gt;&lt;/p&gt;

</description>
      <category>devops</category>
      <category>bash</category>
      <category>documentation</category>
      <category>bashscripting</category>
    </item>
  </channel>
</rss>
