Most people start by thinking of containers as “lightweight VMs.” That mental model works for a few days, then hurts you for years. A container is just a process (or a few) with isolation and a custom filesystem view, built from an image. Once you see it that way, a lot of “Docker magic” becomes predictable.
This article walks through container internals, lifecycle, core commands, resource limits, and a practical troubleshooting mindset.
1. Mental Model: What a Container Really Is
Under the hood, a container is:
- A Linux process (PID on the host)
- Running with:
- Its own filesystem view (from an image + writable layer)
- Its own network namespace (own IP stack)
- Its own PID namespace (process tree inside container)
- Resource limits enforced by cgroups
Key idea:
If the container is “running,” that means there is a process running on the host. If the process exits, the container stops. There’s no “guest OS” like a VM.
You can prove this to yourself:
# Run a simple container
docker run --name demo -d alpine:3.19 sleep 1000
# On the host, find the PID
docker top demo
# or
ps aux | grep sleep
You’ll see the sleep process; that’s your container.
2. Container Lifecycle: From Image to Running Process
For a single container, the lifecycle looks like this:
- Create
Docker allocates metadata, filesystem, network namespace, etc.
(You can do this explicitly withdocker create, butdocker rundoes it implicitly.) - Start
Docker launches the container’s main process (the ENTRYPOINT+CMD from the image). - Running
The process is alive. Docker tracks its stdout/stderr, resources, and exit code. - Stopped
The process exits. The container object + writable layer are still present on disk. - Removed
Docker deletes the container’s metadata and filesystem layer.
The main command you use:
docker run
is a convenience wrapper for:
docker createdocker start
Understanding that helps when you debug:
- A container that “exits immediately” is just a process that finishes quickly.
- To keep it running, you need a long‑running process (server, tail, sleep, etc.).
3. Creating and Running Containers: Core Patterns
3.1 One‑off interactive containers
Use these whenever you want a temporary shell:
bash
docker run --rm -it alpine:3.19 sh
Flags breakdown:
--rm→ delete container when it stops.-it→ interactive TTY (so you get a shell).
Mental model: this is your “disposable debugger” or “scratch VM,” but it’s still just a process.
3.2 Long‑running services
Typical pattern for APIs, web servers, etc.:
bash
docker run -d --name web -p 8080:80 nginx:1.25-alpine
-d→ detached mode (run in background).--name web→ stable name for logs, exec, etc.-p 8080:80→ map host port 8080 to container port 80.
This container:
- Is backed by a main process (
nginxmaster process). - Will stop if that process crashes or exits.
If it keeps dying, don’t think “VM crashed”; think “process crashed” and check logs.
3.3 Environment variables and configuration
Pass configuration at runtime:
docker run -d --name orders-api \
-p 8080:8080 \
-e SPRING_PROFILES_ACTIVE=prod \
-e DB_HOST=db \
myorg/orders-api:1.0.0
This becomes:
- Environment variables visible to the process inside the container.
- Exactly like
exporton a normal Linux host.
This is usually the right place for non‑secret configuration (URLs, flags, modes). Secrets should be handled more carefully in real systems (secret managers, etc.).
4. Managing Containers Day‑to‑Day
Think of these as your daily driver commands.
4.1 Listing containers
docker ps # running only
docker ps -a # all containers (running + stopped)
Useful columns:
- NAMES → what to use with
logs,exec,inspect. - STATUS → “Up 5 minutes”, “Exited (0) 2 seconds ago”.
4.2 Stopping, starting, removing
docker stop web # send SIGTERM, wait, then SIGKILL after timeout
docker start web # restart a stopped container
docker rm web # remove container (must be stopped)
If you want to kill and recreate:
docker rm -f web # force remove (stop + rm)
Typical dev loop:
docker rm -f webdocker build -t myorg/web:dev .docker run ...
4.3 Logging: stdout and stderr as your primary log sink
Docker automatically captures the main process’s stdout and stderr:
docker logs web # historical logs
docker logs -f web # follow logs (tail -f)
Good practice:
- Your app should log to stdout/stderr (not to local files inside the container).
- That way, orchestrators (Compose, Swarm, Kubernetes, log collectors) can pick up logs easily.
For a Spring Boot service:
- Configure logs to go to console → Docker (and later Kubernetes) can aggregate them.
4.4 Exec into containers
When something is weird, “enter” the container:
docker exec -it web sh
# or for Debian/Ubuntu based images:
docker exec -it web bash
Use this to:
- Inspect filesystem.
- Run curl, ping, or app‑specific debug commands.
- Quickly check config files and environment vars (
env).
It’s equivalent to SSHing into a VM, but you’re really just attaching to a process’s namespace.
5. Resources: Making Sure Containers Don’t Eat the Host
Because containers share the host kernel, they can starve each other if you don’t set limits.
5.1 CPU limits
bash
docker run -d --name cpu-demo --cpus="1.0" myorg/task:1.0.0
This roughly constrains the container to 1 CPU core worth of time. Without limits, one container can saturate the host CPU, especially on dev machines.
5.2 Memory limits
bash
docker run -d --name mem-demo --memory="512m" myorg/task:1.0.0
- If the process allocates more than that, the kernel may kill it with OOM (Out Of Memory).
- You’ll see exit code 137 (killed) or similar in Docker.
Combine:
docker run -d --name api \
-p 8080:8080 \
--cpus="1" \
--memory="512m" \
myorg/orders-api:1.0.0
This is closer to how you’d run things in production.
5.3 Checking usage: docker stats
docker stats
Gives live CPU, memory, network, I/O usage per container.
If one service is misbehaving, this is your quick “top” for containers.
6. Restart Policies: Making Containers Survive Crashes
In pure Docker (without an orchestrator), restart policies give minimal self‑healing:
docker run -d --name api \
--restart=on-failure \
-p 8080:8080 \
myorg/orders-api:1.0.0
Common policies:
no(default): never restart automatically.on-failure: restart only if exit code ≠ 0.always: always restart if stopped.unless-stopped: restart unless you manually stopped it.
Use cases:
on-failurefor tasks that might crash but shouldn’t be resurrected if cleanly completed.always/unless-stoppedfor long‑running services on standalone hosts.
Later, in Kubernetes, this concept maps to Pod restart behavior controlled by the controller (Deployment, etc.).
7. Containers vs Images vs Volumes: How Changes Persist
A common confusion: “I edited a file inside the container, but when I recreate it, my changes are gone.”
Key rules:
- Images are immutable.
- Container writable layer is ephemeral:
- If you
docker rmthe container, changes in that layer vanish.
- If you
- Volumes are persistent:
- They outlive containers and can be attached to new ones.
Workflow implication:
- To change the app code or binaries, you usually:
- Change source.
- Rebuild image.
- Start new container from new image.
- To persist data (database, uploads):
- Use volumes, not the container’s writable layer.
8. Debugging Containers: A Practical Playbook
When something “doesn’t work,” follow a steady sequence.
8.1 Container exits immediately
- Check status:
bash
docker ps -a - Inspect exit code and logs:
bash
docker logs my-container
Common causes:
- Wrong command in
CMD/ENTRYPOINT(executable not found). - Main process completes and exits (e.g., script finishing).
- Crash due to missing config/env.
Fix: make sure the main process is long‑running and configured correctly.
8.2 Container running, but port not accessible
Checklist:
-
Is the container running?
bash
docker ps -
Is the app actually listening on the right port inside the container?
docker exec -it api sh# inside container:netstat -tulnp # or ss -tulnpMany apps bind to
127.0.0.1; inside a container, that’s still the container only.
You usually want to bind to0.0.0.0. -
Is the port mapped on the host?
docker ps # look at PORTS column, e.g. 0.0.0.0:8080->8080/tcp -
Can you curl from the host?
curl http://localhost:8080/health
If it works inside the container but not outside:
- The app might only listen on localhost inside the container.
- Or the port mapping (
-p) is wrong/missing.
8.3 Container can’t reach another container (DB, cache, etc.)
Checklist:
- Are they on the same Docker network?
- Is the dependency container running?
- Is your app using the container name as hostname (on user‑defined networks)?
Debug:
docker exec -it api sh
# inside api container:
ping db
apk add --no-cache curl
curl http://db:5432 # or appropriate protocol/port
If DNS name doesn’t resolve, check that both are attached to the same user‑defined network and not using the default bridge incorrectly.
9. Containers in the Bigger Picture: Why This Mental Model Matters
Once you internalize:
- Container = process with isolation.
- Image = filesystem + metadata.
- Volume = persistent data.
then:
- Debugging Docker is just debugging Linux processes with extra tooling.
- Moving to Kubernetes is easier because Pods are also just wrapper abstractions around containers/processes.
- You stop expecting “VM‑like” behaviors (like “I changed a file and it should persist forever”) and design images + volumes properly.