{
"$type": "site.standard.document",
"bskyPostRef": {
"cid": "bafyreihsusy4rh5jesludgr6ccfhu46zm2ynqnytqgg2kelwftgg4q3yx4",
"uri": "at://did:plc:z2irh2ogqmggn53qmpyi3e7s/app.bsky.feed.post/3mee2uqutet32"
},
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreiht5semacddkxcxs53dnfvz3lu5pavovzvzmblr7wszba7l42udem"
},
"mimeType": "image/png",
"size": 185762
},
"description": "I am reviving a failed university prototype and rebuilding it as my first SaaS. This first post strips away the mess of understanding of Docker, from the Copy-on-Write file system and layer caching to persistent volumes and container networking.",
"path": "/skills-lab/zero-to-saas-01-lets-finally-understand-docker/",
"publishedAt": "2026-02-08T13:43:26.000Z",
"site": "https://niklas-heringer.com",
"tags": [
"**containers**",
"WSL",
"Dockerfile",
"repo",
"layers",
"View it now!"
],
"textContent": "Hey folks! Hope you all got well into 2026!\n\nI definitely did, along with great amibitions for this blog and my other online channels.\n\n📎\n\nExpect ****way more content**** on here!\n\nThis'll be one of my _first series_ of 2026.\n\nLast year around this time, I had built a showcase for a software in Uni.\n\nIt's techstack was garbage and it would've never worked, yet the **idea** for it is.. rather cool in my opinion. I will unveil more about it _bit by bit._\n\nI’m bringing that idea back from the dead. This time, I’m building it for real, production-ready, secure, and scalable, together with a good friend of mine.\n\nI’ll be documenting every single learning of this journey right here.\n\nWe're starting from scratch. And today's first topic's going to be: **Docker**(already scared?). Alright, _let's get started._\n\n# What even IS Docker?\n\n📦\n\nDocker is a ****set of services**** that use __OS-level__ virtualization to run self-contained instances of software.\n\nThese _self-enclosed packages_ are called **containers**; they include _everything_ that the software running within needs to function, **nothing** else is needed.\n\nSuch execution environments can range from tiny blobs with a few files to fully blown Linux distributions (in which case, one of these included packages for the container to use could be `apt` to manage dependencies).\n\n## You don't need to allocate ANYTHING\n\n> In virtualization, you'd need to predefine how much RAM, space etc. a VM has, but _not here._ Docker _takes what it needs_**on the run!**\n\n* * *\n\n## Small Excursus: How Docker runs\n\nDocker runs as a service, how this occurs depends on the host OS:\n\n * **Linux:** Runs as an OS daemon service (such as a `systemd` unit file), which is then sent commands via the `docker` and `docker-compose` clients.\n * **macOS:** A Xen Hypervisor Virtual Machine runs the Docker service, and a UI and CLI client connects to it.\n * **Windows:** A Virtual Machine (called MobyVM) is run, with the UI and CLI client connecting to it, or via WSL.\n\n\n\n* * *\n\n# Terminology: Understanding Docker names\n\n## Layers/ Image Layers\n\nA layer is a change on an _image_(to which we'll come to in a second), or an _intermediate image_ as the layer at the top can represent the current overall status.\n\nEvery command, some of which you might've already encountered, `FROM`, `RUN`, `COPY` and so on, all these written in the Dockerfile cause the _previous image_ to **change** , thus **creating a new layer.**\n\nYou can think of it as staging changes when you're using git: You add a file's change, then another one, then another one...\n\nConsider the following Dockerfile (inspired from this great repo):\n\n\n FROM rails:onbuild\n ENV RAILS_ENV production\n ENTRYPOINT [\"bundle\", \"exec\", \"puma\"]\n\nFirst, we choose a starting image: `rails:onbuild`, which in turn has many layers. We add another layer on top of our starting image, setting the environment variable `RAILS_ENV` with the `ENV` command. Then, we tell docker to run `bundle exec puma` (which boots up the rails server). That's another layer.\n\nThe concept of layers comes in handy at the time of building images.\n\n> Because layers are intermediate images, if you make a change to your Dockerfile, docker will rebuild **only** the layer that was changed and the ones after that. This is called **layer caching**.\n\n## Images\n\nAn image is the self-contained object **representing the file system _,__comprised of Docker layers._**\n\nIn an image, there are therefore $x$ layers, with $1 < x < n, n \\in \\mathbb{N}$.\n\nImages are created from **Dockerfile recipes,** just as we saw in the layers example above.\n\n### Docker Images vs Dockerfiles\n\n> This analogy might be crude, but i think it conveys the point:\n\n * The Dockerfile is the set of instructions, like a **class** in Object-Oriented Programming is.\n * The resulting image is the product of said instructions, the **object**.\n\n\n\n## Container\n\nWhen an image is selected to run, a new container is created that _uses the layers_ from the Docker image as the **base filesystem** and then runs any command that is given on it.\n\n> The image acts as the template for the container and the container **never** alters the image.\n\nWhat if the container creates new/ updates files?\n\n## The Copy-on-Write (CoW) Strategy\n\nTo understand how Docker stays so efficient, you have to look at the **Copy-on-Write** mechanism. When you start a container, Docker doesn't copy the entire image. Instead, it adds a thin **writable layer** on top of the stack of read-only image layers.\n\nEverything that happens during the container's runtime is recorded in this top layer:\n\n * **Creating new files:** These go directly into the writable layer.\n * **Modifying existing files:** Docker performs a \"copy-up.\" It copies the file from the read-only image layer to the writable layer first, then applies your changes there. The original file remains untouched in the image below.\n * **Deleting files:** Docker creates a **\"Whiteout\"** file in the writable layer. This acts as a mask, telling the system to act as if the file doesn't exist, even though the original is still physically present in the read-only layer below.\n\n\n\n> **The Golden Rule:** Container layers only live as long as the container itself. Once you `docker rm` that container, the writable layer, and all your changes, are **gone.**\n\n## Persistent Data: Volumes\n\nSince container layers are ephemeral, we need a way to keep data like databases or user uploads _alive_. This is where **Volumes** come in. Think of a volume as a \"bridge\" that bypasses the writable container layer and writes directly to the host’s file system.\n\nThere are two primary ways to handle this:\n\n 1. **Bind Mounts:** You point to a specific, existing directory on your host machine (like `/home/user/config`) and map it to a path in the container. This is great for development where you want to sync code changes instantly.\n 2. **Named Volumes:** You let Docker manage the storage location. You give it a name (e.g., `db_data`), and Docker handles the \"where\" on the host disk. This is the cleaner, more \"production-ready\" approach.\n\n\n\n## Networking: Breaking the Isolation\n\nBy default, containers are isolated. If your web server container needs to talk to your database container, they need a **Docker Network**.\n\nMost of the time, you'll use a **Bridge Network**. It acts like a _private virtual switch_ ; containers on the same bridge can find each other by their container names. If you need a container to appear as if it’s running directly on your host’s network without any isolation, you’d use the **Host** driver.\n\n> Any container that needs to **send** or **receive** data from the internet or other containers requires a properly configured network interface.\n\n## Registries and Repositories\n\nFinally, how do we share these images? We use **Registries**.\n\nA **Registry** (like Docker Hub or GitHub Packages) is the service that _hosts_ the data. Inside a registry, you have **Repositories** , which are collections of different versions of the same image, distinguished by **Tags** (e.g., `postgres:14` vs `postgres:latest`).\n\nThe lifecycle is simple:\n\n 1. **Build** your image from a Dockerfile.\n 2. **Push** it to a _remote_ Registry.\n 3. **Pull** it down onto any other machine in the world to run your software exactly as it was built.\n\n\n\n> This is a crucial pin-point for how we want to use Docker in our project.\n\n* * *\n\n# The Docker CLI\n\nNow that you understand the theory of layers and volumes, here is how you actually talk to the Docker daemon. Think of this as your \"Day 1\" survival kit:\n\n#### Docker CLI Cheatsheet\n\nGet my ****Docker Core CLI Cheatsheet**** for free. One page, all the essential commands, no filler.\n\nView it now!\n\n* * *\n\nTo round out the \"Basics\" of this first part, you must move from manual CLI commands to **Orchestration**.\n\nRunning one container with a long `docker run` command is fine for testing, but real apps have multiple moving parts (a frontend, a backend, and a database). You don't want to type out 50 arguments every time you start your stack.\n\nThe next logical stage is **Docker Compose**.\n\n* * *\n\n# Docker Compose (Infrastructure as Code)\n\nDocker Compose is a tool for defining and running multi-container applications. Instead of manual commands, you define your entire stack in a single file: `docker-compose.yml`.\n\n## The `docker-compose.yml` Structure\n\nThis file uses **YAML** syntax to define services, networks, and volumes.\n\n\n version: '3.8'\n services:\n web:\n build: . # Tells Docker to use the Dockerfile in the current dir\n ports:\n - \"8080:80\" # Map Host 8080 to Container 80\n volumes:\n - .:/code # Bind mount for live development\n networks:\n - backend\n\n db:\n image: postgres:15\n environment:\n POSTGRES_PASSWORD: example_password\n volumes:\n - db_data:/var/lib/postgresql/data\n networks:\n - backend\n\n volumes:\n db_data: # Named volume for persistence\n\n networks:\n backend: # Isolated network for the two containers\n\n\n## The \"Up & Down\" Workflow\n\nCompose simplifies the entire lifecycle into two main commands:\n\n 1. **`docker-compose up -d`**\n * Reads the YAML file.\n * Builds the images.\n * Creates the networks and volumes.\n * Starts all containers in the correct order.\n 2. **`docker-compose down`**\n * Stops all containers.\n * Removes the containers and the internal networks.\n * _Note:_ It keeps your volumes intact so your data isn't lost.\n\n\n\n## Why this is a Game Changer\n\n * **Version Control:** Your infrastructure setup is now a file in your Git repo.\n * **One Command Setup:** A new developer joins your project? They run `docker-compose up` and the entire environment is ready.\n * **Service Discovery:** Inside the `backend` network, the `web` container can reach the database simply by using the hostname `db`. No IP addresses required.\n\n\n\n* * *\n\n# What’s Next: From Theory to Reality\n\nWe’ve laid the groundwork. You know how Docker thinks, how it stores data, and how Compose orchestrates the chaos. But theory only gets us so far.\n\nIn the next post of this series, we’re getting our hands dirty. We will:\n\n 1. **Define the Tech Stack:** I'll finally unveil the core components of this \"revived\" SaaS.\n 2. **The First Dockerfile:** We will write a production-ready Dockerfile (no more \"garbage tech stacks\").\n 3. **Local Dev Environment:** Setting up a hot-reloading development environment using the Docker Compose skills we just covered.\n\n\n\n> **The ambitious goal for 2026 is simple:** Build in public, fail fast, and document the \"why\" behind every architectural decision.\n\nSee you in the next one. We’re just getting started. 🚀",
"title": "Zero to SaaS 01: Let's FINALLY understand Docker",
"updatedAt": "2026-02-08T13:43:26.000Z"
}