kleamerkuri

kleamerkuri

Jul 8, 2026 · 17 min read

This Is How To Stop Guessing What Docker Actually Does

The most recent “works on my machine” moment was a custom MCP server I built for my team that ran fine on my Mac. But a few of my teammates are on Windows, and the second they tried running it through WSL, it spit out errors.

From what I could tell, it came down to two things layered on top of each other:

  • WSL’s path handling didn’t play nicely with the stdio transport the server used
  • There was a SQLite issue tangled up in there too, since SQLite was handling the indexing

I’m not going to pretend I fully untangled it because I don’t work in WSL, and debugging someone else’s environment through a screen share is beyond frustrating 🫣

The takeaway is that the environment mismatch that caused the issue is exactly the kind of mismatch Docker solves for.

Different Node version, missing environment variable, some system library they didn’t have installed—pick your poison. Docker packages an application with everything it needs to run into one portable unit, so it behaves the same way no matter what machine it lands on.

In this post, I’m going to show you what that actually looks like by building something that watches Docker while you learn it.

That’s not a typo. You’re going to deploy a tool called Portainer, which provides a visual dashboard for everything happening in a Docker environment. Then you’re going to run basic Docker commands in the terminal and watch them play out live in that dashboard, in the browser, in real time.

You’ll stop and start containers from the UI and watch the terminal reflect it. You’ll even step inside a running container and poke around like it’s its own little computer because, in a lot of ways, it is šŸ’ā€ā™€ļø

By the end, you won’t just know the definitions of “image” and “container.” You’ll have watched the relationship between them happen in front of you, from two different angles at once.

Why “It Works on My Machine” Isn’t Just a Joke

Before touching any commands, let’s understand the problem Docker was actually built to solve, because the mental model makes everything downstream easier.

When you build an application, it doesn’t run in isolation. It depends on a specific version of a language runtime, specific libraries, specific environment variables, sometimes a specific operating system quirk you didn’t even know you were relying on (my MCP case in point).

On your machine, all of that happens to line up.

On a teammate’s machine, or on a production server, it might not.

That mismatch is where “works on my machine” comes from, and it’s less funny once it’s cost you an afternoon of debugging something that isn’t even in your code šŸ˜’

Docker solves this by packaging an application together with everything it needs to run:

  • Runtime
  • Dependencies
  • Configuration

All three go into a unit that behaves the same way everywhere it’s deployed. Instead of hoping every environment matches, you ship the environment along with the app.

That packaged unit is called a Docker image, and it’s a read-only template. Think of it as a set of instructions describing exactly what should exist inside the environment, such as which:

  • base operating system files to start from
  • packages to install
  • files to copy in
  • command to run when it starts

It’s kind of like a recipe. It tells you exactly what to do, but a recipe sitting in a book isn’t dinner.

A Docker container is what you get when you actually run that image. If the image is the recipe, the container is the actual cake—baked, sitting on the counter, doing something (like enticing you to eat it šŸ°)

You can start multiple containers from the same image, like you can bake the same recipe more than once. Each container runs independently, with its own writable space layered on top of the image’s read-only instructions, but they all trace back to the same set of instructions.

Tip šŸ¤“
If a container behaves unexpectedly and you’re not sure whether the problem is in the image or the running container, ask yourself which one you’d need to throw away and rebuild to actually fix it. When the fix requires changing what gets installed or copied in, that’s an image problem. When it’s something specific to this one running instance, it’s a container problem.

Honestly, this distinction still confuses me when I’m deep in a project and moving fast. But once that sticks, terms like “pulling an image” and “running a container” stop being jargon and start being literal descriptions of what’s happening.

Why This Post Uses Portainer Instead of Just a Terminal

Most Docker intros dump you into a terminal and have you run commands against text output that scrolls by and disappears. You get through the tutorial, but you don’t necessarily see what changed. You have to trust that it worked, or run another command to check.

I want you actually to watch it happen.

So instead of just running commands, you’re going to launch a real, useful piece of software—Portainer—that gives you a graphical dashboard for your Docker environment. Portainer isn’t a toy built for this tutorial; it’s a real tool people run in production to manage containers, images, volumes, and networks without memorizing every flag.

Using it here means every command you type in the terminal has a visible, immediate effect somewhere you can watch, like a browser tab showing your own Docker environment, live.

Something is clarifying about watching a system observe itself šŸ¤–

You’ll run docker pull, and instead of just trusting the output, you’ll flip to your browser and see that image appear in a list. That feedback loop is the whole point of this approach.

How to Install Portainer With Docker (Step by Step)

Portainer runs as a container itself, using Docker to manage Docker, which is a good early taste of how flexible containers actually are.

You’ll need Docker installed and running before starting (Docker Desktop if you’re on Mac or Windows, or the Docker Engine directly if you’re on Linux).

Note: I’m not covering installation in detail here. I assume you’ve already got Docker installed and running, since setup varies a bit by OS. I’ll pick up from there.

Step 1: Create a Volume for Portainer’s Data

A volume is a storage location managed by Docker that lives outside any single container, so the data survives even if the container running Portainer gets removed and recreated:

docker volume create portainer_data

Tip: You can use the terminal directly inside of Docker Desktop. Simply click on the bottom right where it says “>_ Terminal” and copy paste the above command.

Step 2: Pull and Run the Portainer Container

docker run -d \
  -p 8000:8000 \
  -p 9443:9443 \
  --name portainer \
  --restart=always \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v portainer_data:/data \
  portainer/portainer-ce:lts

Here’s what each piece of that command is actually doing, because dropping a wall of flags on you without context isn’t helpful:

  • d runs the container in detached mode, meaning it runs in the background instead of tying up your terminal
  • p 8000:8000 and p 9443:9443 map ports on your machine to ports inside the container. Port 9443 is where you’ll reach the dashboard; port 8000 is only needed if you later use Portainer’s Edge Agent features, but it’s standard to include
  • -restart=always tells Docker to bring the container back up automatically if it stops or your machine restarts
  • v /var/run/docker.sock:/var/run/docker.sock is the important one because it gives Portainer access to the Docker socket, which is how Docker’s daemon (the background process managing containers) receives commands. Without this, Portainer would have nothing to talk to
  • v portainer_data:/data connects the volume you created in Step 1, so Portainer’s own settings and login persist
  • portainer/portainer-ce:lts is the image itself, the Community Edition, pinned to the long-term support tag instead of a version that might change under you

Now, if you’re totally new and have never actually run Portainer before, you won’t have that recipe (aka image), and you’ll see in the logs that Docker can’t find it locally, so it’ll automatically pull it.

docker run -d \
  -p 8000:8000 \
  -p 9443:9443 \
  --name portainer \
  --restart=always \
  -v /var/run/docker.sock:/var/run/docker.sock \
  -v portainer_data:/data \
  portainer/portainer-ce:lts
Unable to find image 'portainer/portainer-ce:lts' locally <-- SEE HERE šŸ‘€ -->
lts: Pulling from portainer/portainer-ce
4f4fb700ef54: Pull complete
5e7726831620: Pull complete
9ac279addb3c: Pull complete
2ff83ba51e76: Pull complete
5c8568fa3dc5: Pull complete
7b49070ee0f3: Pull complete
1df8821a82a1: Pull complete
d89867feba2c: Download complete
321ddad92c47: Download complete
Digest: sha256:5f9b4bda5582fc72c07d730f86168205f4042d82c9cde011c9146b12496e4625
Status: Downloaded newer image for portainer/portainer-ce:lts
778245fc0c6fed6d8f3f601a35d92ac04d6f38fcc1a6013ae20a46f2d4f5a423

Then, when you click into the volume you created, you’ll see the items in that recipe appear along with a running container.

Step 3: Log Into Your New Dashboard

Open https://localhost:9443 in your browser. Your browser will likely flag the connection as insecure—that’s expected. Do not change the protocol to http.

Portainer generates a self-signed certificate by default, so you can click through the warning safely on a local setup.

You’ll be prompted to create an admin account before you can access the dashboard, and depending on your Portainer version, you may also need a setup token that Docker printed to the container’s logs. If that screen appears, run docker logs portainer in your terminal, and you’ll find the token in the output as setup_token.

Tip šŸ‘‡
Portainer only gives you five minutes to finish this first-time setup before the service inside the container shuts itself down for security. If you get pulled away, simply run docker restart portainer and the setup screen will be waiting for you again.

Once you’re in, Portainer automatically detects your local Docker environment. (If you happen to have more than one, select “local”.)

You’ll land on a dashboard showing your images, containers, volumes, and networks—probably mostly empty right now, aside from Portainer itself.

Note: Portainer is now managing the Docker environment it’s running inside of. That’s not a bug or a weird edge case but exactly what you want for this exercise, and it’s how a lot of people run Portainer for real.

Explore: The Privacy Tools You Trust Aren’t Doing What You Think

The Mirror Effect: Watching Docker Commands Show Up Live in Portainer

Here’s where the “mirror” idea actually pays off. Keep your Portainer dashboard open in one browser tab, and open a fresh terminal window alongside it.

Pull an image you haven’t used yet. Alpine Linux is a good choice because it’s small and fast to download:

docker pull alpine

Remember, this is pulling your “recipe.”

Switch to your Portainer tab and click into Images in the sidebar. You’ll see alpine sitting in the list, with its size and the tag you pulled.

Nothing dramatic happened in the terminal beyond a progress bar, but Portainer just gave you a visual receipt of the same action.

Now run a container from that image:

docker run -d --name my-alpine alpine sleep 3600

That command starts a container named my-alpine, using the image you just pulled, and runs sleep 3600 inside it so the container stays alive for an hour instead of exiting immediately. Alpine has no long-running process by default, so without something to keep it busy, the container would start and stop in the same instant.

Refresh (or just glance at) the Containers section in Portainer, and my-alpine shows up there too, marked as running. You typed two commands in a terminal, and a browser-based dashboard reflected both of them without you telling it to refresh or sync anything.

That’s the whole idea in action. Portainer is talking to the same Docker daemon your terminal is talking to. There’s no separate database, no sync delay to think about—it’s the same environment, viewed two different ways.

Stopping, Starting, and Removing Containers From the Terminal and the Browser

Now flip the direction. In Portainer, find my-alpine in your container list and click the Stop button next to it.

Back in your terminal, run:

docker ps

docker ps lists your currently running containers, and you’ll notice my-alpine is missing from that list now because you stopped it from the browser, not the terminal.

Tip: If you want to see it anyway, docker ps -a shows all containers regardless of state, and you’ll find it there marked as exited.

Start it again from Portainer, then run docker ps a second time. It’s back.

Stop, start, and remove all work exactly this way regardless of which side you trigger them from, because both interfaces are just different windows into the same underlying system.

There’s no “real” way to manage Docker that the terminal has and the GUI doesn’t, or vice versa. They’re both just clients talking to the same daemon.

How to Step Inside a Running Docker Container (docker exec Explained)

Reading the definition of “containers share the host’s kernel” never made sense to me the way it did when I actually got dropped into one.

A kernel is the core part of an operating system that manages hardware and processes underneath everything else.

A running Docker container isn’t just a background process sitting there. It behaves like its own small operating system, with its own filesystem and its own process list, even though it’s sharing that kernel with the machine it’s running on.

Opening a Shell From the Terminal

You can prove that to yourself by opening a shell inside a running container. In your terminal:

docker exec -it my-alpine sh

What it’s doing:

  • docker exec runs a command inside an already-running container
  • it flags keep that session interactive, so instead of the command running and immediately exiting, you get dropped into a live shell
  • sh, since Alpine doesn’t ship with bash by default

Poking Around Inside Alpine

Once you’re in, poke around as you would on any Linux machine:

ls /
cat /etc/os-release
ps aux

You’ll see a full filesystem and an OS identifying itself as Alpine Linux.

docker exec -it my-alpine sh
/ # ls
bin    etc    lib    mnt    proc   run    srv    tmp    var
dev    home   media  opt    root   sbin   sys    usr
/ # cat /etc/os-release
NAME="Alpine Linux"
ID=alpine
VERSION_ID=3.24.1
PRETTY_NAME="Alpine Linux v3.24"
HOME_URL="<https://alpinelinux.org/>"
BUG_REPORT_URL="<https://gitlab.alpinelinux.org/alpine/aports/-/issues>"

Check the process list, though, and you’ll notice it only shows what’s running inside this container, not everything running on your actual computer.

/ # ps aux
PID   USER     TIME  COMMAND
    1 root      0:00 sleep 3600
    7 root      0:00 sh
   15 root      0:00 ps aux

That’s the isolation Docker provides. The container can see its own world, not everyone else’s, even though underneath it all it’s sharing your machine’s kernel rather than running a separate operating system, as a full virtual machine would.

Type exit when you’re done, and you’ll drop back into your regular terminal, with the container still running in the background.

Doing the Same Thing From Portainer’s UI

Portainer gives you the same capability without leaving the browser.

Open a container’s detail page and look for the Console option, which opens an interactive shell inside that container right in the UI. Same underlying mechanism as docker exec, just with buttons instead of flags.

Note šŸ“ā€ā˜ ļø
Because containers share the host’s kernel, a compromised container has a shorter distance to the rest of your system than a compromised virtual machine would. That’s a real tradeoff of Docker’s lightweight isolation. It’s good to be aware but not a reason to panic over a local learning setup like this one.

What We Actually Did Here

If you’ve followed along this far, you’ve watched the same action—pulling an image, running a container, stopping it, stepping inside it—from two different angles, and confirmed for yourself that they’re two views of one thing, not two separate systems you have to keep in sync.

That’s the mirror effect this whole post is built around: two tools staring at the same reality and agreeing with each other, live.

It’s the part that doesn’t come across from reading a definition of “image” and “container” on its own. Watching docker pull show up in a dashboard you didn’t refresh, or watching a container you stopped from a browser disappear from docker ps, makes the relationship concrete in a way that’s visual.

But, in my usual fashion, I did still have a question: If Docker Desktop already comes with its own dashboard, why bother with Portainer at all? Aren’t they doing the same job?

Not quite. Docker Desktop is what actually gets Docker’s daemon running on your Mac or Windows machine in the first place by bundling that daemon (running inside a lightweight virtual machine under the hood) along with a basic built-in dashboard. Portainer doesn’t replace any of that.

Portainer needs a Docker daemon already running somewhere to talk to, which is exactly why you installed Docker before installing Portainer earlier in this post.

What Portainer adds on top is a more capable, dedicated dashboard that’s built to point at this machine, a coworker’s server, or a dozen environments at once, not just the one Docker Desktop already has you looking at.

Note šŸ‘€
For this post, that difference didn’t matter much since everything ran on one machine, so either dashboard would’ve shown the same containers. However, you didn’t just install a fancier version of something you already had. You installed a separate dashboard built to manage far more than one machine’s worth of containers, pointed here at the same underlying system.

You’ve also got a real tool sitting on your machine now, not just a tutorial exercise. The next time you’ve got five containers running for a side project and can’t remember which port you mapped to which one, Portainer is still there.

It’s a Wrap

None of this required memorizing a long list of Docker commands up front. You pulled an image, ran a container, stopped and started it from two different places, and stepped inside one to look around.

Every time, you had a second window confirming what actually happened instead of just trusting the terminal output.

If you want to push this further before moving on, try pulling a different image, something like nginx or redis are both small and give you something slightly different to poke around inside. Then repeat the mirror exercise on your own without the commands in front of you. Notice what you remember without looking it up.

This is how Docker earns your trust: by watching it, poking at it, confirming for yourself that it behaves the way it says it does. Part Two is where that trust gets put to work šŸ˜‰

I’m building a local memory agent next. It’s a self-hosted setup running Ollama with a RAG workflow that indexes your own private files so you can query them with local AI, no cloud round-trip required. Same underlying tool, but this time Docker isn’t just something to observe but what makes the whole thing possible.

Don’t miss it. I’ll see ya then āœŒļø

šŸ˜ Don’t miss these tips!

We don’t spam! Read more in our privacy policy

Related Posts

Leave a Comment

Your email address will not be published. Required fields are marked *