Docker Introduction
Docker is a platform for packaging and running applications in isolated environments called containers. It matters because it lets you ship an application with its operating-system libraries, runtime, and startup command so it behaves consistently on a laptop, test server, or cloud host. Docker is not a full virtual machine; it shares the host kernel while isolating processes, filesystems, networking, and resource usage.
Overview: How Docker Works
At the center of Docker are three ideas: an image, a container, and a registry. An image is a read-only template made from stacked filesystem layers. A container is a running or stopped instance of that image with a thin writable layer on top. A registry, such as Docker Hub or a private registry, stores image manifests and layer blobs so machines can pull the same image by name and tag.
When you type docker run nginx:1.27-alpine, the Docker CLI sends a request to the Docker daemon. On Linux, the daemon creates the container using kernel features such as namespaces, control groups, and a union filesystem. On Docker Desktop for macOS or Windows, those Linux features run inside a small Linux VM managed by Docker Desktop. The user experience is mostly the same, but paths, file sharing, and performance can differ when bind mounting host files.
An image name like nginx:1.27-alpine points to a tag. The tag resolves to a manifest, and the manifest lists the content-addressed layers that make up the image. Docker downloads layers it does not already have, verifies them by digest, and stores them locally. Multiple images can share layers, which is why pulling a second related image can be fast.
A container usually runs one main process. If that process exits, the container stops. The container may have its own filesystem view, network interface, hostname, environment variables, and process tree, but it is still using the host kernel. This is why containers start quickly and use fewer resources than full VMs, but also why you should not treat containers as a security boundary equal to a separate physical machine.
Syntax
The general Docker CLI shape is:
docker [GLOBAL OPTIONS] COMMAND [COMMAND OPTIONS] [ARGUMENTS]
| Part | Meaning |
|---|---|
docker |
The client program you run in your terminal. |
GLOBAL OPTIONS |
Options that affect the client connection, such as selecting a context. Beginners usually omit these. |
COMMAND |
The operation, such as run, pull, build, ps, stop, rm, or images. |
COMMAND OPTIONS |
Flags for that command. For docker run, common flags include --name, -d, -p, -e, and -v. |
ARGUMENTS |
Usually an image name, container name, file path, or command to run inside the container. |
The most common first command is docker run:
docker run [OPTIONS] IMAGE [COMMAND] [ARGUMENTS]
| Option | Use |
|---|---|
--name web |
Assigns a predictable container name instead of a random one. |
-d |
Detached mode: run in the background and print the container ID. |
-p 8080:80 |
Publishes host port 8080 to container port 80. |
-e KEY=value |
Sets an environment variable in the container. |
-v name:/path |
Mounts a named volume or bind mount for persistent data or local development files. |
--rm |
Automatically removes the container when it exits. |
Examples
Run a Test Container
docker run --rm hello-world:latest
Output:
Hello from Docker!
This message shows that your installation appears to be working correctly.
This downloads the hello-world image if it is not already present, creates a container from it, runs the image’s default command, prints a short message, and exits. The --rm flag removes the stopped container automatically. The image remains cached locally, so a later run does not need to download the same layers again.
Run a Web Server and Publish a Port
docker run -d --name web-intro -p 8080:80 nginx:1.27-alpine
docker ps --filter name=web-intro
docker stop web-intro
docker rm web-intro
Output:
a3f5c7d9e1b2
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
a3f5c7d9e1b2 nginx:1.27-alpine "/docker-entrypoint..." Up 3 seconds 0.0.0.0:8080->80/tcp web-intro
web-intro
web-intro
The first command starts Nginx in the background and publishes the container’s port 80 on the host’s port 8080. Visiting http://localhost:8080 reaches the Nginx process inside the container. The docker stop command sends a stop signal and waits briefly before forcing shutdown if needed. The docker rm command deletes the stopped container, but it does not delete the nginx:1.27-alpine image.
Build a Small Image
FROM python:3.12-alpine
WORKDIR /app
COPY app.py .
EXPOSE 8000
CMD ["python", "app.py"]
This Dockerfile expects an app.py file in the build directory. The pinned base image tag python:3.12-alpine is more reproducible than python:latest. The EXPOSE 8000 line documents that the application listens on port 8000, but it does not publish that port to your host. You still need -p when running the container.
docker build -t intro-python:1.0 .
docker run --rm -p 8000:8000 intro-python:1.0
Output:
[+] Building 2.1s (8/8) FINISHED
Successfully tagged intro-python:1.0
Serving application on port 8000
The build sends the current directory as the build context, executes each Dockerfile instruction, and stores the resulting image as intro-python:1.0. The run command starts a container from that image and maps host port 8000 to container port 8000.
How It Works Step by Step
- The Docker CLI parses your command and sends an API request to the Docker daemon through a local socket or named pipe.
- The daemon checks whether the requested image exists locally. If it does not, Docker contacts the configured registry, resolves the tag to a manifest, and downloads missing layers.
- Docker creates a container metadata record: name, image, command, environment variables, mounts, networking, labels, and restart policy.
- Docker prepares the filesystem by mounting the image’s read-only layers and adding a thin writable layer for that container.
- Docker creates network plumbing. By default, a container joins Docker’s bridge network and gets its own internal IP. Published ports are forwarded from the host to the container.
- The daemon starts the configured process. Logs written to stdout and stderr are captured by Docker’s logging driver and can be read with
docker logs. - When the main process exits, the container stops. Its writable layer remains until you remove the container, which is why stopped containers can still consume disk space.
This model explains several beginner surprises. Changing a file inside a running container changes only that container’s writable layer, not the original image. Removing an image can fail if a container still references it. Pulling an image can be fast because Docker only downloads layers missing from the local store.
Common Mistakes
Assuming EXPOSE Publishes a Port
FROM nginx:1.27-alpine
EXPOSE 80
This metadata is useful, but it does not make localhost:80 work. Publish the port when you run the container:
docker run --rm -p 8080:80 nginx:1.27-alpine
Using latest for Production
docker run -d --name production-web nginx:latest
The latest tag is only a tag name, not a promise of stability. It can move when the image publisher releases a new version. Use a specific tag, and for high-control production workflows consider pinning by digest after testing.
docker run -d --name production-web nginx:1.27-alpine
Baking Secrets into Images
FROM alpine:3.20
ENV API_KEY=changeme
RUN echo "$API_KEY" > /tmp/key.txt
RUN rm /tmp/key.txt
This is still wrong even though the file is removed later. Image layers are historical records; data written in an earlier layer can remain recoverable from that layer. Put secrets in runtime environment variables, Docker secrets, mounted files, or your orchestrator’s secret store, never in a Dockerfile.
Forgetting Containers Are Disposable
If a database writes data only inside the container filesystem, deleting the container deletes that data. Use a named volume for persistent service data and a bind mount for local source-code editing. Volumes are managed by Docker; bind mounts point at a specific host path.
Best Practices
- Use specific image tags such as
redis:7.2-alpineornode:20-alpine; avoidlatestexcept for quick experiments. - Name important containers with
--nameso logs, stops, and removals are predictable. - Publish only the ports you need with
-p. Do not assumeEXPOSEopens anything on the host. - Keep images small by starting from appropriate base images and copying only the files the application needs.
- Use a
.dockerignorefile in real projects so build contexts do not includenode_modules, Git history, caches, or local secrets. - Store persistent data in named volumes, not in a container’s writable layer.
- Read logs with
docker logsand inspect container configuration withdocker inspectbefore guessing. - Prefer
docker composefor multi-container applications. The modern command has a space; the olddocker-composebinary is legacy.
Practice Exercises
- Run
nginx:1.27-alpinein detached mode with the namepractice-weband publish it on host port8090. Expected end state:http://localhost:8090reaches Nginx, anddocker psshows one running container. - Start
redis:7.2-alpinewith a named volume mounted at/data. Stop and remove the container, then create a new Redis container using the same volume. Hint: usedocker volume createand-v volume-name:/data. - Create a tiny Dockerfile for a command-line Python script using
python:3.12-alpine. Build it with a versioned tag and run it with--rm. Expected end state: the script output appears, and no stopped container is left behind.
Summary
- Docker packages applications as images and runs them as isolated containers.
- An image is read-only layered content; a container adds a thin writable layer and a running process.
- The Docker CLI talks to the Docker daemon, which pulls images, creates filesystems, configures networking, and starts processes.
- Registries store manifests and layers, so tags can be pulled consistently across machines when you choose stable tags.
EXPOSEdocuments a port;-por Composeports:publishes it.- Containers are disposable by design, so persistent data belongs in volumes or external services.
