Free Ebook cover Docker for Beginners: Containers Explained with Simple Projects

Docker for Beginners: Containers Explained with Simple Projects

New course

12 pages

Images, Containers, and Registries in Practical Use

Capítulo 2

Estimated reading time: 11 minutes

+ Exercise

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 App

Download the app

docker images

You’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:latest

This returns JSON with useful fields such as:

  • Config.Env: environment variables baked into the image
  • Config.Entrypoint and Config.Cmd: what runs by default
  • Config.ExposedPorts: ports the image declares
  • RootFS.Layers: the layer digests

To view the history of layers (helpful for understanding size and caching):

docker history nginx:latest

Tags, 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.0

You 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:stable

In 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 dev or stable for 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 --digests

Cleaning up images safely

As you build and pull images, disk usage grows. Practical cleanup commands include:

docker image prune

This removes dangling images (layers not referenced by any tag). To remove unused images (not used by any container), use:

docker image prune -a

Be 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.27

Key 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 ps

See all containers including stopped ones:

docker ps -a

Understanding 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=web
docker logs web

If you need more detail, inspect the container:

docker inspect web

Look for:

  • State.Status and State.ExitCode
  • State.Error and State.OOMKilled
  • Config.Cmd and Config.Entrypoint to confirm what ran

Following logs and limiting noise

For services, you often want to follow logs in real time:

docker logs -f web

To show only the last lines:

docker logs --tail 50 web

To include timestamps:

docker logs -t web

Executing 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 sh

Some 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.conf
docker cp ./index.html web:/usr/share/nginx/html/index.html

Remember 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 web

Remove all stopped containers:

docker container prune

If 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.0

Parts:

  • 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 login

For a specific registry:

docker login registry.example.com

After login, Docker stores credentials in your Docker config. On shared machines, be mindful of security and log out if needed:

docker logout registry.example.com

Pushing 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.0

Step 2: Push it

docker push mydockerhubusername/myapp:0.1.0

Docker 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.0

Then run it:

docker run --rm mydockerhubusername/myapp:0.1.0

Pulling images and controlling what you get

Pulling is straightforward:

docker pull redis:7.4

But 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 login again, 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 :latest from 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-web
cat > 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> EOF

Create 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 EOF

Step 2: Build the image

docker build -t hello-web:0.1.0 .

List it:

docker images | grep hello-web

Step 3: Run a container and test it

docker run -d --name hello-web -p 8080:80 hello-web:0.1.0

Now open http://localhost:8080 in your browser. If you prefer command-line testing:

curl http://localhost:8080

Check logs:

docker logs hello-web

Stop and remove the container when done:

docker stop hello-web
docker rm hello-web

Step 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.0
docker login
docker push mydockerhubusername/hello-web:0.1.0

Step 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.0
docker pull mydockerhubusername/hello-web:0.1.0
docker run --rm -p 8080:80 mydockerhubusername/hello-web:0.1.0

This 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.2 for a release
  • myorg/myapp:1.4 as a convenience tag pointing to the latest patch in that minor line
  • myorg/myapp:main for the latest build from the main branch
  • myorg/myapp:git-abc1234 for 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 :latest by 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 login and 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 ps should show 0.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 at NetworkSettings.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

Now answer the exercise about the content:

Why is it safer in practical projects to use an explicit version tag or a digest instead of relying on the latest tag?

You are right! Congratulations, now go to the next page

You missed! Try again.

latest can be reassigned at any time, so builds may change unexpectedly. Using a specific version tag makes deployments repeatable, and a digest provides an immutable identity for the exact image content.

Next chapter

Building Reproducible Environments with Dockerfiles

Arrow Right Icon
Download the app to earn free Certification and listen to the courses in the background, even with the screen off.