Career OS

Docker 04 — Running & Operating Containers

You have an image. Now you have to run it, watch it, climb inside it when it misbehaves, and shut it down cleanly — without googling each command mid-incident. This is the module that turns “I built an image” into “I operate containers,” which is the difference between someone who followed a tutorial and someone you can hand a misbehaving service to at 11pm.

The Goal

By the end of this module you can:

  • Explain what the docker CLI actually talks to — and why nothing runs without the daemon
  • Run splitease-api detached with a published port and an injected env var, from memory
  • Read a running container’s logs live and exec a shell inside it to debug
  • Walk any container through its full lifecycle — image to created to running to stopped to removed
  • Debug the two traps that get every beginner: the wrong-way-round port and the container that exits the instant you start it
  • Keep a container up across crashes and reboots with a restart policy

The Lesson

The CLI is not Docker — the daemon is

When you type docker run, the docker command does almost nothing itself. It is a thin client. It packages your request and sends it over a socket to a long-running background process called the daemon (dockerd). The daemon is the thing that actually pulls images, creates containers, runs processes, and streams logs back. The CLI just asks; the daemon does.

flowchart LR
  CLI["docker CLI"] -->|"REST over socket"| Daemon["dockerd daemon"]
  Daemon --> C1["container splitease-api"]
  Daemon --> C2["container postgres"]
  Daemon --> Store["local image store"]
  Store --> Daemon

Three things follow from this picture, and each one saves you confusion later:

  • The daemon must be running. On Windows that means Docker Desktop is started. If the whale icon is off, every command dies with error during connect ... the system cannot find the file specified or Cannot connect to the Docker daemon. The fix is never the command — it is “start Docker Desktop.”
  • The image store is local. When you docker run nginx, the daemon first checks its own local store. Only if the image is missing does it reach out to a registry. That is why the second run is instant.
  • The CLI is stateless. It holds nothing. Every docker ps, docker logs, docker stop is a fresh question to the daemon, which is the single source of truth about what is running. This is why you can close your terminal and a detached container keeps running — the daemon owns it, not your shell.

In a real job this is the mental model behind half of all “Docker is broken” tickets: nine times out of ten the daemon is down or pointed at the wrong host, and the command was fine all along.

docker run = create + start

The one command you will type most is docker run. It does two things in sequence: it creates a container from an image (a fresh writable layer on top of the read-only image), then starts its main process. People think of it as one verb; it is two, and knowing that makes the lifecycle below click.

Here is the shape of a real run, with the flags you will actually use every day:

docker run -d \
  --name splitease \
  -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=dev \
  --restart unless-stopped \
  splitease-api:latest

Read it flag by flag — these six cover ~90% of what you will ever type:

FlagMeansWhy you reach for it
-dDetached. Run in the background, print the container ID, give your prompt back.The default for any server. Without it your terminal is glued to the process.
-p 8080:8080Publish a port: host:container.The only way traffic from your machine reaches the app inside. Direction matters — see below.
-e KEY=valueSet an environment variable inside the container.Inject config without rebuilding the image — DB URL, profile, secrets at runtime.
--name spliteaseGive the container a human name.So you type docker logs splitease, not docker logs a3f9c1.
--rmAuto-remove the container when it stops.For throwaway runs (a quick test, a one-off script) so they do not pile up.
-itInteractive + TTY. Keep stdin open and attach a terminal.For anything you want to type into — a shell, a REPL. The opposite of -d.

-d and -it are opposites in intent: detached for “run it and leave it,” interactive for “I want to talk to it.” You rarely use both.

Trap #1 — the port direction. -p 8080:8080 reads host:container. Left of the colon is your machine; right is inside the container. If splitease-api listens on 8080 inside but you want to hit it on 9090 from your laptop, that is -p 9090:8080. Get them backwards (-p 8080:9090) and curl localhost:9090 works while localhost:8080 connects to nothing — and the container looks perfectly healthy, which is what makes this one so maddening. Say it out loud every time: host first, container second.

Seeing what is running: ps, logs, inspect

Once it is running detached, you have lost the live console — on purpose. Here is how you get the view back.

docker ps              # containers running right now
docker ps -a           # ALL containers, including stopped and exited ones

docker ps shows you the living. The moment a container stops it vanishes from ps — which is exactly when beginners panic and think it was deleted. It was not. docker ps -a shows it sitting there in Exited state, and the STATUS column (Exited (1) 4 seconds ago) is your first clue about why it died.

docker logs splitease       # everything the app printed to stdout/stderr
docker logs -f splitease    # follow: stream new lines live, like tail -f

docker logs is your primary debugging window. Whatever the app writes to standard out and standard error, the daemon captured it — even for a container that already crashed. The -f (follow) flag tails it live so you can watch a request flow through in real time.

docker inspect splitease    # full JSON: IP, mounts, env, ports, restart policy, exit code

docker inspect dumps everything the daemon knows about the container as JSON — its internal IP, mounted volumes, the exact env it was started with, its restart policy, and its exit code. You will not read all of it; you will Ctrl-F for the one field you need ("ExitCode", "IPAddress"). Treat it as the reference manual, not bedtime reading.

Getting inside: docker exec

This is the single most useful operational skill in the whole module. Your container is running but something is wrong — the app cannot reach the database, a config file looks empty, an env var seems unset. You do not rebuild and pray. You walk inside the running container and look:

docker exec -it splitease sh

exec runs a new command inside an already-running container. With -it and sh (or bash if the image has it), that command is an interactive shell — and now you are standing inside the container’s filesystem, as if you SSH’d into a tiny machine:

# now inside the container:
env | grep SPRING        # are my env vars actually set?
cat /app/application.yml # is the config what I think it is?
wget -qO- localhost:8080/actuator/health   # does the app answer from inside?
exit                     # leave the container shell (it keeps running)

The distinction that matters: docker run starts a container from an image; docker exec runs a command in a container that is already up. Exec touches nothing about the image — it is purely a debugging window into the live thing. In a real incident, exec -it ... sh is the move that turns “it’s broken and I have no idea why” into an actual answer in two minutes.

The lifecycle: create, start, stop, remove

Every container walks the same path, and docker run just compresses the first two steps. Step through it below and watch what happens to the writable layer — the scratch space stacked on top of the read-only image where all runtime changes live.

The states, and the commands that move between them:

StateWhat it meansHow you got here
imageA read-only template. Not a container yet.docker build or docker pull
createdContainer exists, writable layer allocated, process not started.docker create (or the create half of run)
runningMain process is alive and executing.docker start (or the start half of run)
stoppedProcess ended; writable layer still on disk.docker stop, or the process exiting
removedContainer and its writable layer gone forever.docker rm
docker stop splitease     # graceful: sends SIGTERM, then SIGKILL after ~10s
docker start splitease    # bring a stopped container back — same writable layer, same data
docker rm splitease       # delete it permanently (must be stopped first)
docker rm -f splitease    # force: stop AND remove in one shot

The load-bearing point — and the visualizer drives it home: stop keeps your writable layer, rm destroys it. Stop a container, start it again, and anything it wrote to its own filesystem is still there. But rm deletes that writable layer for good. So if your app wrote data inside the container (an uploaded file, a SQLite DB, Postgres data in the default location), docker rm vaporizes it. That is the entire reason volumes exist — storage that lives outside the container’s writable layer and survives rm. You will wire those up in data-and-networking; for now just hold the danger in your head: rm is not undo, it is delete.

Trap #2 — “my container exited immediately.” You run it, docker ps shows nothing, docker ps -a shows Exited (0) a second later. The container did not crash — a container lives exactly as long as its main process. If that process is a script that finishes, or a shell with nothing to do, or a command that printed and returned, the container’s whole reason to exist is over and it stops. A container is not a little always-on VM; it is a wrapper around one foreground process. To stay up it needs a process that keeps running in the foreground — a web server, java -jar app.jar, a daemon that blocks. splitease-api stays up because the JVM runs forever waiting for requests. A container running sh with no -it exits at once because sh reads end-of-input and quits. Check the exit code in docker ps -a or docker inspect: Exited (0) means “the process finished normally — there was just nothing to keep it alive.”

Keeping it up: restart policies

By default a stopped container stays stopped, and a crashed one stays dead. For anything you actually depend on, tell the daemon to bring it back:

docker run -d --restart unless-stopped --name splitease -p 8080:8080 splitease-api:latest
PolicyBehaviour
no (default)Never auto-restart.
on-failureRestart only if it exits non-zero (a crash), up to a retry limit.
unless-stoppedAlways restart it — across crashes and host reboots — unless you explicitly stopped it.
alwaysLike unless-stopped, but restarts even ones you manually stopped, after a daemon restart.

unless-stopped is the sane default for a service on your own box: it survives crashes and reboots, but respects a deliberate docker stop so you can take it down on purpose without it springing back. This is the daemon-as-supervisor feature — the same job systemd or a process manager does, built right in. Real orchestrators (Kubernetes, in the Cloud track) take this idea much further, but the local instinct starts here.

Build This

Operate splitease-api end to end — run, observe, climb inside, tear down. You need the image from dockerfile-deep-dive; if you do not have it yet, build it first with docker build -t splitease-api:latest . from the repo root. Run everything below from any terminal with Docker Desktop running.

# 1. Run it detached, published on 8080, with a profile env var, kept up across crashes
docker run -d \
  --name splitease \
  -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=dev \
  --restart unless-stopped \
  splitease-api:latest

# 2. Confirm it is alive and note the STATUS column
docker ps

# 3. Hit it from your host — proves the published port works
curl http://localhost:8080/actuator/health
#    expected: {"status":"UP"}

# 4. Tail the logs live (Ctrl-C to stop following — the container keeps running)
docker logs -f splitease

# 5. Climb inside the RUNNING container and look around
docker exec -it splitease sh
#    now inside:
env | grep SPRING          # SPRING_PROFILES_ACTIVE=dev should be there
wget -qO- localhost:8080/actuator/health   # the app answering from inside itself
exit

# 6. Tear it down cleanly
docker stop splitease       # graceful shutdown
docker ps -a                # see it now in Exited state, not gone
docker rm splitease         # delete it for good
docker ps -a                # gone

Definition of done: You ran a server detached, reached it over a published port from your host with curl, watched its logs stream live, opened a shell inside the running container and confirmed your env var landed, then stopped and removed it — and you can explain, without looking anything up, why -p 8080:8080 is host:container, why the container stayed up (long-running JVM process) while a bare sh would have exited, and what docker rm destroyed that docker stop left alone.

Where to Practice

WhatWhereTime
Run/ps/logs/exec/stop on a throwaway image, no installlabs.play-with-docker.com30 min
docker run reference — every flag, in orderdocs.docker.com (CLI reference → docker run)20 min
Container lifecycle + restart policies guidedocs.docker.com (Get Started → run containers)20 min
Operate your own splitease-api exactly as in Build Thisyour SplitEase repo30 min

How to Practice

Run the Build This loop three times across three days from memory, deleting the container at the end each time so you start clean. Day one, peek at this page; day three, type every command cold. The retention drill that matters most: deliberately break it — run with -p 9090:8080 and curl the wrong port, or run an image whose process exits, then use docker ps -a and docker logs to diagnose why before you read the answer. Operating containers is muscle memory; you only build it by doing the moves, not reading them.

Check Yourself

1. What does the docker CLI actually talk to, and what happens if it is not running?

Answer

The CLI talks to the daemon (dockerd) over a socket — the daemon does all the real work (pulling, creating, running, logging). If the daemon is not running (Docker Desktop not started), every command fails with Cannot connect to the Docker daemon and the fix is to start Docker, not change the command.

2. In -p 8080:9090, which number is your machine and which is inside the container?

Answer

host:container. So 8080 is your machine, 9090 is inside the container. You would reach the app at localhost:8080, and it must be listening on 9090 inside for this to work.

3. docker run is secretly two operations. Which two?

Answer

Create (make a container from the image, allocate its writable layer) then start (run its main process). docker create + docker start separately do the same thing.

4. Your container exits the instant you start it, docker ps -a shows Exited (0). What happened?

Answer

Its main process finished. A container lives exactly as long as its foreground process — if that process is a script that returns, or a shell with no input, there is nothing to keep the container alive. Exit code 0 means it ended normally, not a crash. It needs a long-running foreground process (a web server, java -jar) to stay up.

5. What is the difference between docker run and docker exec?

Answer

docker run starts a new container from an image. docker exec runs a command inside a container that is already running — your main debugging window. exec -it <c> sh gives you an interactive shell inside the live container.

6. You docker stop a container, then docker start it again. Is the data it wrote still there? What about after docker rm?

Answer

After stop + start: yes, the writable layer survives, so the data is still there. After docker rm: norm destroys the writable layer permanently. Data you need to keep belongs in a volume (see data-and-networking).

7. Why does docker ps show nothing while docker ps -a shows your container?

Answer

docker ps lists only running containers. The moment one stops it disappears from ps but still exists on disk in Exited state — docker ps -a shows all containers including stopped ones, with the STATUS column telling you when and how it exited.

8. Which restart policy keeps a service up across crashes and reboots but still lets you take it down on purpose?

Answer

--restart unless-stopped. It restarts the container after crashes and host reboots, but respects a deliberate docker stop (it will not spring back until you start it again).

Still Unclear?

Paste any of these to Claude to go deeper:

  • “Walk me through what actually happens inside the Docker daemon, step by step, when I run docker run -d -p 8080:8080 splitease-api. Where does the port mapping get set up, and what process owns it?”
  • “Give me five different reasons a container might exit immediately with docker ps -a showing Exited (1), the exact log line or symptom for each, and how to confirm which one I’m hitting.”
  • “Explain the difference between docker stop, docker kill, and docker rm -f in terms of signals (SIGTERM vs SIGKILL) and what each does to my app’s in-flight requests and writable layer.”

Why AI Can’t Do This For You

AI can hand you the docker run line, but it cannot stand at your keyboard at 11pm when the container is up, the logs look fine, and curl localhost:8080 still hangs. Diagnosing that — is it the port direction, a process that exited, a daemon pointed at the wrong host, an env var that never landed — is a sequence of live observations (ps -a, logs -f, exec -it sh, inspect) that only you can run against your actual running system, reading each result to decide the next move. The commands are learnable in an afternoon; the operating instinct of which to reach for, in what order, under pressure, is the skill that makes you the person who gets handed the broken service.

Module done? Add it to today’s tracker

Saves your progress on this device.