Containers, Docker & Docker Compose
A practical guide to container isolation, images and layers, Dockerfiles, build and run commands, registries, volumes, networking, and Docker Compose for wiring multi-container applications together.
Java 301 · Deployment
How to package an application with everything it needs so it runs the same on every machine — and the Docker commands that build, run, connect, and orchestrate those packages.
A container bundles your code with its runtime, libraries, and config into one sealed box, so it behaves identically on your laptop, a teammate's machine, and production. Docker is the tool that builds those boxes from a recipe (a Dockerfile), runs them, and — with Docker Compose — starts several at once and wires them together.
Section 1
Notebook Summary#
Dense, hand-copyable revision sheet. Every line here is expanded in the Detailed Notes below.
Why containers#
- SDLC = 7 phases → focus here: Development → Testing → Deployment
- Problem: code that runs on my machine breaks on yours (env/lib mismatch)
- Container = seal app + deps in one box → runs identically everywhere cargo ship: mixed cargo → identical boxes → stack/ship any way
Container vs VM#
| Container | VM | |
|---|---|---|
| Size | small (app + deps) | big (full OS) |
| Start/stop | fast | slow |
| Kernel | shares host's | own kernel |
| Isolation | weaker | stronger |
Docker core#
- Docker = open platform to build / ship / run apps in containers
- Image = blueprint (read-only) · Container = running instance (+ read-write layer) Image : Container :: Class : Object
- OCI = standards body → 3 specs: Runtime, Image, Distribution → portability
- Registry = image store (DockerHub default; also ECR, ACR, GCR, Quay)
Dockerfile → image → container#
- Dockerfile = text recipe. Instructions UPPERCASE by convention
- FROM parent image (must be 1st) · WORKDIR set dir · RUN build-time cmd (new layer) · CMD run-time cmd
- RUN = at build. CMD = at docker run. Don't swap them
- Build: docker build . -t name:tag (custom file → -f path)
Ports & networking#
- -p host:container — order is HOST first. docker run -p 8000:8080
- Each container = own network namespace (isolated stack)
- Default net = bridge. Modes: bridge · none · host (not channel)
- Two containers talk only if on the same network two boxes on one rail = talk; on separate rails = silence
- Net cmds: network create · network connect <net> <cid> · network ls
Docker Compose#
- docker-compose.yml = declare many containers + net + volumes in 1 file
- Keys: version · services · build/context/dockerfile · ports · networks · depends_on
- Cmds: build → up (-d detach) → down · custom file: -f
- Compose replaces manual network create + many docker run
Command cheat sheet#
| docker pull <img> | fetch image from registry |
| docker build . -t n:t | build image from Dockerfile |
| docker run -p h:c <img> | start a container |
| docker ps | list running containers |
| docker stop <id> | stop a container |
| docker rm <id> | remove a container |
| docker image rm <img> | remove an image (containers first!) |
Section 2
Detailed Study Notes#
You finish a feature on Friday, it works perfectly on your laptop, and you push it. Monday morning a teammate pulls the branch and nothing runs — a different Java version here, a missing library there, an environment variable no one wrote down. The code never changed. The surroundings did. Everything below exists to kill that failure mode: package the surroundings with the code, once, and ship them together.
2.1The deployment problem#
Software is built in phases, and the last one — getting it onto users' machines — is where "works on my machine" bites.
A software project moves through a life cycle of about seven phases. For this lesson only the last three matter: the development phase (splitting the project into modules and turning requirements into code), the testing phase (validation and quality assurance — catching functional, UX, and security issues), and the deployment phase (delivering the finished product to its intended users and making it actually usable). Deployment is the step where the environment mismatch above shows up.
Picture loading a ship with cargo of every shape and size — furniture, fruit, machines. Loading each item loose is chaos. Instead you put everything into standardized shipping containers: now any item can be packed, and once packed the container can be stacked, craned, and transported the same way regardless of what's inside. Software containers do the same thing — a uniform box around wildly different applications.
2.2What is a container#
A container is a lightweight, self-contained box that holds everything a piece of software needs to run — the code, a runtime, libraries, environment variables, and config files — all sealed together and isolated from other containers.
Packing for a trip, you put clothes, books, and snacks into one suitcase and you're ready to go; at your destination you unpack exactly what you brought. A container is a suitcase for your application: it carries everything the app needs, so wherever it lands it has its own supplies and doesn't depend on what's already there.
Because each container bundles its own dependencies and is isolated from the others, the same container behaves the same way on any machine that can run containers. That reproducibility is the whole point.
The natural next question is: isn't that just a virtual machine? Not quite. A virtual machine (VM) carries a full copy of an operating system, so it's large and slow to spin up but strongly isolated — it has its own kernel. A container carries only the app and its dependencies and shares the host machine's kernel, so it's small, fast to create and destroy, but less fully isolated from the host.
The single sentence that answers "container vs VM" in an interview: containers share the host's kernel; VMs each run their own. That one fact explains every other difference — smaller size, faster start, weaker isolation.
2.3The Open Container Initiative (OCI)#
The OCI is a group of companies that agreed on shared container standards so a container built by one tool runs anywhere.
Those standards give containers two properties. Portability means everything needed to run the software lives inside the container. Interoperability means any vendor that supports the standard can run the container without changes. The OCI defines three specifications:
How to run a container.
The physical structure of a container image.
How to distribute images.
2.4Docker: image vs container#
Docker is an open platform to create, deploy, and run applications in containers. It shrinks the gap between writing code and running it in production, so you ship faster and stop worrying about setting up environments by hand.
Two Docker terms get confused constantly, and the distinction is the most important thing in this lesson. A Docker image is a reusable, shareable, read-only file — a template built from your code, its dependencies, and a Dockerfile. A Docker container is a running instance created from an image: it takes the image's read-only layers and adds a writable layer on top, and it's the thing that actually runs your application.
Image is to container as class is to object in Java. The image is the blueprint you write once; each container is a live instance stamped out from it. You can run many containers from one image, just as you instantiate many objects from one class.
Where do images come from? A registry is a cloud repository of ready-made images. The default is DockerHub (docker.io); others include AWS ECR, Azure ACR, Google GCR, and Red Hat Quay. Pulling an image mirrors cloning from Git:
docker pull hello-world # fetch a prebuilt image from a registry
docker run hello-world # create + start a container from it
docker ps # list running containers
docker rm <container-id> # remove a container (by id or name)
docker image rm hello-world # remove the image itselfYou cannot remove an image while a container created from it still exists. Remove the containers first (docker rm), then the image (docker image rm). Skipping this gives you a "conflict: unable to delete" error, not silence.
2.5The Dockerfile#
A Dockerfile is a plain text recipe listing the commands Docker runs to build an image automatically.
Instructions are not case-sensitive, but the convention is UPPERCASE so they stand out from their arguments. Comments start with #. Four instructions carry this lesson:
# Sample Dockerfile
FROM ubuntu:latest
WORKDIR /app
RUN ["apt-get", "update"]
CMD echo Hello World from Docker| Instruction | When it runs | Role |
|---|---|---|
| FROM | build | Parent image to build on. Must be the first instruction (only ARG may precede it). |
| WORKDIR | build | Optional. Sets the working directory inside the container. |
| RUN | build | Executes a command in a new layer and commits the result. RUN cmd or RUN ["exe","p1"]. |
| CMD | docker run | The default command executed when the container starts. Same two forms as RUN. |
RUN happens at build time and bakes its result into the image as a new layer. CMD happens later, at docker run time, and defines what the container does when it starts. Putting your startup command in RUN (so it fires during build and never again) is a classic mistake.
Build an image from the recipe with docker build:
# "." is the build context (current dir); -t sets the image name:tag
docker build . -t hello-world-2
# point at a custom filename with -f
docker build . -t hello-world-3 -f Dockerfile-server2.6Port mapping#
Port mapping opens a door on the host and forwards everything arriving there into a port inside the container.
By default a container's ports are sealed inside it. Port mapping exposes a service running in the container to the host machine or the outside network: it links a host port to a container port so that a request hitting the host port is automatically forwarded into the container. The flag is -p, and the order is host first, container second.
# run detached (-d), map host 8000 -> container 8080
docker run -d -p 8000:8080 hello-world-3
# now the host can reach the containerized server
curl -X POST -H "Content-Type: application/json" \
-d '{"username": "barveabhishek-ab"}' http://localhost:80002.7Docker networking#
Docker networking lets containers talk to each other and the outside world, while keeping each one's network isolated.
It's built on the Linux kernel's network namespace — a feature that gives a group of processes their own private network stack. Every container gets its own namespace, so its own network stack. When you start a container it's automatically attached to a default network called the bridge, which lets it reach other containers and the outside.
By default, two containers can only talk to each other if they're on the same network. Two services that "should" communicate but sit on different networks will simply fail to reach one another — with no obvious error. Put both on one custom network.
Docker supports three network modes: bridge (the default, attaches to the default bridge network), none (disconnects the container from all networks), and host (gives the container full access to the host's network stack). The commands:
docker network create mynetwork # make a custom network
docker network connect mynetwork <container> # attach a running container
docker run --network mynetwork name:tag # start already attached
docker network ls # list all networks2.8Docker Compose#
Docker Compose manages many containers at once from a single YAML file, instead of running and wiring them by hand.
Real apps are rarely one container — a server, a client, a database. Starting each with its own docker run and manually creating networks is tedious and error-prone. Compose declares the whole system in one docker-compose.yml: its services (containers), networks, and volumes (persistent storage). A few terms: services are the containers, networks connect them, volumes persist data.
Everything you did by hand in the two sections above — docker build per image, docker network create, and one docker run per container with the right --network and -p flags — collapses into one file and one command. That's the entire value proposition.
version: '3' # compose file format; v3 is standard
services:
server:
build:
context: . # build context = current dir
dockerfile: Dockerfile-server
ports:
- "8000:8080" # host:container (needed only if host must reach it)
networks:
- mynetwork
client:
build:
context: .
dockerfile: Dockerfile-client
depends_on:
- server # start server before client
networks:
- mynetwork
networks:
mynetwork: # the shared network both services joinThree commands drive it:
docker-compose build # build all images from the yml
docker-compose up # start every service (add -d to run detached)
docker-compose down # stop containers, remove their networks, clean up
# custom filename:
docker-compose -f my-compose.yml upCompose looks for docker-compose.yml by default; -f points it at another file. The depends_on key sets start order, and because both services join mynetwork they can talk to each other by service name — no manual networking.
2.9Interview & practice Q&A#
What is the difference between a Docker image and a Docker container?
An image is a read-only template — code, dependencies, and a Dockerfile baked into shareable layers. A container is a running instance of an image, with a writable layer added on top. One image can spawn many containers, exactly like one class can instantiate many objects.
How is a container different from a virtual machine?
A container shares the host's kernel and packages only the app plus its dependencies, so it's small and fast to start but less isolated. A VM bundles a full guest operating system with its own kernel, so it's larger and slower but more strongly isolated. Containers win on speed and efficiency; VMs win when you need full OS isolation.
What does the OCI define and why does it matter?
The Open Container Initiative is an industry group that publishes three specifications — Runtime, Image, and Distribution. Together they make containers portable (self-contained) and interoperable (runnable on any compliant platform), so you're not locked to one vendor's tooling.
What is the difference between RUN and CMD in a Dockerfile?
RUN executes at build time and commits its result as a new image layer — use it for installing packages and preparing the image. CMD defines the default command that runs when the container starts via docker run. RUN shapes the image; CMD runs the app.
What does -p 8000:8080 mean, and which side is the host?
It maps host port 8000 to container port 8080. The format is always host:container — the left number is the port on your machine, the right is the port inside the container. A request to localhost:8000 is forwarded to :8080 inside the container.
Why might two running containers fail to communicate?
By default containers can only reach each other if they're attached to the same network. If they're on different networks (or one is on the default bridge and the other on a custom network), traffic won't flow. Create a custom network and attach both, or define them under one network in Compose.
What are Docker's three network modes?
bridge — the default, connecting the container to the default bridge network. none — disconnects the container from all networks. host — gives the container full access to the host's network stack. (There is no "channel" mode.)
What problem does Docker Compose solve?
It defines and runs multi-container applications from one YAML file. Instead of building images, creating networks, and running each container by hand, you declare services, networks, and volumes once and control them with docker-compose build/up/down.
Why can't you delete an image that still has containers?
Containers depend on their source image's read-only layers, so Docker refuses to remove an image while any container built from it exists. Remove the containers first with docker rm, then the image with docker image rm.
2.10Key takeaways#
- A container seals an app with its dependencies so it runs identically everywhere; it shares the host kernel, unlike a VM.
- An image is the read-only blueprint; a container is a running instance of it (class → object).
- A Dockerfile is the recipe: FROM · WORKDIR · RUN (build time) · CMD (run time). Build with docker build . -t name:tag.
- Port mapping is -p host:container — host first.
- Containers talk only on the same network; modes are bridge · none · host.
- Compose declares many services + networks + volumes in one docker-compose.yml, driven by build / up / down.
Section 3
Microexercises#
Reading proves you recognise the idea; these prove you can use it. Attempt each before opening the walkthrough.
3.1Build it — a Dockerfile and its commands#
Write a Dockerfile and the two commands to build and run it.
- Base it on ubuntu:latest.
- Set the working directory to /app.
- Update the package list at build time.
- When the container starts, it should print Hello World from Docker.
- Then: build it tagged hello-world-2, and run it.
Expected result: running the container prints Hello World from Docker and exits.
Show the full walkthrough
Step 1 — pick the right instruction for each requirement. "Base it on" is FROM (and it must come first). "Working directory" is WORKDIR. "At build time" means RUN. "When the container starts" means CMD — not RUN, because startup output must fire at run time, not build time.
Step 2 — write the Dockerfile.
# Sample Dockerfile
FROM ubuntu:latest
WORKDIR /app
RUN ["apt-get", "update"]
CMD echo Hello World from DockerStep 3 — build and run. . is the build context and -t names the image.
docker build . -t hello-world-2
docker run hello-world-2
# -> Hello World from DockerWhy: RUN baked the package update into the image once; CMD is what actually executes each time you start a container from it — the build-time vs run-time split in action.
3.2Diagnose it — why can't the client reach the server?#
A teammate starts two containers and the client cannot reach the server. Predict exactly why, then fix it with the fewest changes.
docker network create mynetwork
# server: attached to the custom network, port mapped
docker run -d -p 8000:8080 --network mynetwork server:latest
# client: started with no --network flag
docker run -d client:latest
# client tries to call the server by name and times out- Name the root cause.
- Predict the observed failure.
- Fix it two ways: at run time, and for an already-running client.
Show the full walkthrough
Root cause: the client was started with no --network, so it landed on the default bridge network, while the server is on mynetwork. Containers can only communicate when they share a network.
Predicted failure: the client's request never reaches the server and the call times out (or fails to resolve the server's name). The server itself is healthy — this is purely a networking mismatch, so there's no crash and no obvious error in the server logs.
Fix A — start the client on the right network:
docker run -d --network mynetwork client:latestFix B — attach the client that's already running:
docker network connect mynetwork <client-container>Why: this is the same-network rule as a reflex — the moment two containers "should" talk but don't, check that they're on one network before touching anything else. (In practice you'd sidestep the whole thing by declaring both under one network in a Compose file.)