Why these three pieces matter in day-to-day Docker work
When you use Docker on a real project, you constantly move between three related things: images (the packaged filesystem and metadata), containers (a running instance created from an image), and registries (places to store and share images). Practical Docker work is mostly about moving cleanly between these states: build an image, run containers from it, inspect what happened, then push/pull the image through a registry so other machines can run the same thing.
This chapter focuses on practical use: how to inspect images and containers, how to tag and version images, how to publish to and consume from registries, and how to troubleshoot common issues that arise in this loop.
Images in practical use
What an image is when you work with it
In practice, an image is a read-only template made of layers. Each layer represents a change (for example, “install packages” or “copy app files”). When you run a container, Docker adds a thin writable layer on top. This layering is why rebuilding can be fast (cached layers) and why images can share common base layers.
What you do most often with images:
- List images on your machine
- Inspect image metadata (entrypoint, command, environment variables, exposed ports)
- Tag images with meaningful names and versions
- Remove old images to free disk space
- Pull images from a registry and push your own
Listing and inspecting images
Start by seeing what you already have locally:
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
docker imagesYou’ll see repositories, tags, image IDs, and sizes. The repository and tag together form the image reference, like nginx:1.27 or myapp:dev.
To inspect an image in detail:
docker image inspect nginx:latestThis returns JSON with useful fields such as:
Config.Env: environment variables baked into the imageConfig.EntrypointandConfig.Cmd: what runs by defaultConfig.ExposedPorts: ports the image declaresRootFS.Layers: the layer digests
To view the history of layers (helpful for understanding size and caching):
docker history nginx:latestTags, versions, and why “latest” is risky
Tags are labels pointing to a specific image. A common beginner mistake is relying on :latest as if it means “the newest stable version.” It does not. It is just a tag that image publishers can move at any time. In practical projects, you should prefer explicit versions (for example, python:3.12.3-slim) so builds are repeatable.
Tagging an image you built locally:
docker tag myapp:dev mydockerhubusername/myapp:0.1.0You can also add multiple tags to the same image ID (for example, a semantic version and a “stable” tag):
docker tag mydockerhubusername/myapp:0.1.0 mydockerhubusername/myapp:stableIn teams, a practical approach is:
- Use semantic versions for releases:
1.2.0 - Use commit-based tags for CI builds:
git-abc1234 - Optionally use moving tags like
devorstablefor convenience, but never as the only reference in production
Image digests: the immutable identity
Tags can move, but digests do not. A digest looks like sha256:... and uniquely identifies the image content. If you need guaranteed immutability, you can pull or run by digest:
docker pull nginx@sha256:... To see digests for local images:
docker images --digestsCleaning up images safely
As you build and pull images, disk usage grows. Practical cleanup commands include:
docker image pruneThis removes dangling images (layers not referenced by any tag). To remove unused images (not used by any container), use:
docker image prune -aBe careful with -a on a machine where you frequently run containers, because it may remove images you expect to reuse.
Containers in practical use
Containers as running processes you manage
A container is an isolated process (or set of processes) started from an image, with its own filesystem view, networking, and runtime configuration. In practical use, you manage containers like you manage processes: start them, stop them, inspect logs, and troubleshoot when something fails.
Common container tasks:
- Run a container with ports and environment variables
- Check if it is running and why it exited
- Read logs
- Execute commands inside it for debugging
- Copy files in/out (occasionally)
- Remove stopped containers
Running a container with a predictable name
When you run containers repeatedly, assign a name so you can reference it easily:
docker run -d --name web -p 8080:80 nginx:1.27Key practical flags:
-d: detached mode (runs in background)--name: stable name instead of a random one-p hostPort:containerPort: publish a port
Check status:
docker psSee all containers including stopped ones:
docker ps -aUnderstanding container exit behavior
A very common real-world issue is: “My container starts and then stops immediately.” This usually means the main process finished or crashed. Check the exit code and logs:
docker ps -a --filter name=webdocker logs webIf you need more detail, inspect the container:
docker inspect webLook for:
State.StatusandState.ExitCodeState.ErrorandState.OOMKilledConfig.CmdandConfig.Entrypointto confirm what ran
Following logs and limiting noise
For services, you often want to follow logs in real time:
docker logs -f webTo show only the last lines:
docker logs --tail 50 webTo include timestamps:
docker logs -t webExecuting commands inside a running container
When debugging, you may need to inspect files, environment variables, or network connectivity from inside the container. Use docker exec:
docker exec -it web shSome images use bash, but many minimal images only include sh. Once inside, you can run commands like ls, cat, or check configuration files.
If the container is not running, docker exec won’t work. In that case, you can start a new container for investigation or use logs and inspect output.
Copying files to and from containers
Copying is not the main workflow for production, but it is useful for quick debugging or extracting generated files:
docker cp web:/etc/nginx/nginx.conf ./nginx.confdocker cp ./index.html web:/usr/share/nginx/html/index.htmlRemember that changes made inside a container’s writable layer are not automatically part of the image. If you want changes to be repeatable, you should rebuild the image (or adjust the build instructions) rather than manually editing running containers.
Removing containers and avoiding clutter
Stopped containers accumulate. Remove one container:
docker rm webRemove all stopped containers:
docker container pruneIf you want a container to be automatically removed when it exits (useful for one-off commands), add --rm when running it:
docker run --rm alpine:3.20 echo "hello"Registries in practical use
What a registry does for you
A registry stores images so they can be pulled to other machines. In practical workflows, registries enable:
- Sharing images across a team
- Deploying the same image to test and production
- CI/CD pipelines that build once and deploy many times
- Versioning and rollback by pulling older tags
Docker Hub is a common public registry, but many organizations use private registries (cloud provider registries or self-hosted). The commands are similar: you tag images with the registry/repository name, log in, then push and pull.
Image naming: registry, namespace, repository, tag
An image reference typically looks like this:
registry.example.com/teamname/myapp:1.0.0Parts:
- Registry host:
registry.example.com(optional; if omitted, Docker Hub is assumed) - Namespace: often a username or organization/team
- Repository: the image name, like
myapp - Tag: version label, like
1.0.0
Logging in to a registry
Before pushing to a registry that requires authentication, log in:
docker loginFor a specific registry:
docker login registry.example.comAfter login, Docker stores credentials in your Docker config. On shared machines, be mindful of security and log out if needed:
docker logout registry.example.comPushing an image: step-by-step
Assume you have a local image called myapp:dev and want to publish it.
Step 1: Tag the image with the registry/repository
docker tag myapp:dev mydockerhubusername/myapp:0.1.0Step 2: Push it
docker push mydockerhubusername/myapp:0.1.0Docker uploads layers. If some layers already exist in the registry (for example, shared base layers), they won’t be uploaded again.
Step 3: Verify by pulling on the same or another machine
docker pull mydockerhubusername/myapp:0.1.0Then run it:
docker run --rm mydockerhubusername/myapp:0.1.0Pulling images and controlling what you get
Pulling is straightforward:
docker pull redis:7.4But in practical environments, you should control versions. Prefer explicit tags. If you must use a moving tag like stable, consider also recording the digest you actually deployed so you can reproduce it later.
Private registries and common friction points
When using private registries, typical issues include:
- Authentication failures: run
docker loginagain, verify permissions, and ensure you’re pushing to the correct namespace - Wrong image name: if you forget the registry host, Docker may try Docker Hub instead
- Network/proxy issues: corporate proxies can block registry access; Docker may need proxy configuration
- Tag collisions: pushing
:latestfrom multiple branches can overwrite what others expect
Practical mini-project: build once, run locally, publish, run elsewhere
This mini-project demonstrates the full loop: create a tiny web image, run it as a container, then push it to a registry and pull it back.
Step 1: Create a small project folder
Create a directory and add a simple HTML file:
mkdir hello-web && cd hello-webcat > index.html << 'EOF' <!doctype html> <html> <head><meta charset="utf-8"><title>Hello</title></head> <body> <h1>Hello from a Docker image</h1> </body> </html> EOFCreate a Dockerfile that serves this file using Nginx:
cat > Dockerfile << 'EOF' FROM nginx:1.27-alpine COPY index.html /usr/share/nginx/html/index.html EOFStep 2: Build the image
docker build -t hello-web:0.1.0 .List it:
docker images | grep hello-webStep 3: Run a container and test it
docker run -d --name hello-web -p 8080:80 hello-web:0.1.0Now open http://localhost:8080 in your browser. If you prefer command-line testing:
curl http://localhost:8080Check logs:
docker logs hello-webStop and remove the container when done:
docker stop hello-webdocker rm hello-webStep 4: Tag for your registry and push
Replace mydockerhubusername with your Docker Hub username (or use your private registry host):
docker tag hello-web:0.1.0 mydockerhubusername/hello-web:0.1.0docker logindocker push mydockerhubusername/hello-web:0.1.0Step 5: Pull and run from the registry reference
To simulate “another machine,” you can remove the local tag and pull again:
docker rmi mydockerhubusername/hello-web:0.1.0docker pull mydockerhubusername/hello-web:0.1.0docker run --rm -p 8080:80 mydockerhubusername/hello-web:0.1.0This demonstrates the key practical point: the registry reference becomes the portable handle for running the same artifact anywhere.
Working with multiple environments: dev vs release tags
In real projects, you often have at least two “tracks” of images:
- Development images: built frequently, may include extra debugging tools, tagged with branch names or commit hashes
- Release images: built from a clean, controlled process, tagged with semantic versions
A simple, practical tagging scheme might look like:
myorg/myapp:1.4.2for a releasemyorg/myapp:1.4as a convenience tag pointing to the latest patch in that minor linemyorg/myapp:mainfor the latest build from the main branchmyorg/myapp:git-abc1234for an exact CI build
This makes it easy to roll back: if 1.4.2 has an issue, you can deploy 1.4.1 by pulling that tag.
Troubleshooting the image-container-registry loop
Problem: “It works on my machine” after pushing
Symptoms: you build locally, run successfully, push, then someone else pulls and it behaves differently.
Practical checks:
- Confirm you pushed the tag you think you pushed:
docker push myorg/myapp:0.1.0(not:latestby accident) - Confirm the other machine pulled the same tag (or digest)
- Inspect the image config on both machines:
docker image inspect myorg/myapp:0.1.0 - Check architecture mismatches (for example, ARM vs x86). If you are on Apple Silicon and others are on x86, you may need multi-arch images in more advanced setups
Problem: Push denied / repository does not exist
Symptoms: denied: requested access to the resource is denied or similar.
Practical fixes:
- Run
docker loginand verify you are logged into the correct registry - Make sure the repository name includes your namespace:
username/repo - Check that you have permission to push to that organization or project
Problem: Container can’t be reached on the expected port
Symptoms: container is running but curl localhost:8080 fails.
Practical checks:
- Verify port publishing:
docker psshould show0.0.0.0:8080->80/tcp - Confirm the service listens on the container port you mapped
- Check logs:
docker logs <container> - Inspect container configuration:
docker inspect <container>and look atNetworkSettings.Ports
Problem: Image is huge and slow to pull
Symptoms: pushing/pulling takes a long time, storage grows quickly.
Practical actions:
- Check image size:
docker images - Review layer history:
docker history myimage:tag - Prefer smaller base images when appropriate (for example, alpine or slim variants) and avoid copying unnecessary files into the image build context
- Remove unused images and containers:
docker container prune,docker image prune