Skip to main content
Back to module
DevOps & Cloud Delivery · Containers

Docker basics

Reading42mLesson 1 of 3 in this module

Package a small app into a Docker image with a Dockerfile, run it as a container and clean up afterwards.

Why it matters

"It works on my machine" is one of the oldest problems in software. An app that runs on a developer laptop can fail on a server because the operating system, the language version or a system library is different. Containers solve this by packaging the app together with everything it needs to run. The same package runs on your laptop, in a CI pipeline and in the cloud.

Cloud engineers meet containers everywhere: CI jobs run inside them, Kubernetes schedules them, and managed services such as AWS ECS, Azure Container Apps and Google Cloud Run deploy them. Docker is the most common tool for building and running them, so it is the starting point for this course.

The core idea: images and containers

Two words do most of the work:

  • An image is a read-only package: a filesystem snapshot (your code, the language runtime, libraries) plus metadata such as which command to start. Images are built once and shared through a registry such as Docker Hub or GitHub Container Registry.
  • A container is a running instance of an image. It gets its own isolated filesystem, process list and network interface, but it shares the host's kernel. That is why a container starts in about a second, while a virtual machine boots a whole operating system.

You can start many containers from one image, just as you can open many windows of one program. When a container is deleted, any changes made inside it are lost unless you stored them in a volume (more on that in the next lesson).

A Dockerfile, line by line

You describe an image in a text file called Dockerfile. Here is one for a tiny Python web app that uses only the standard library:

# app.py
from http.server import BaseHTTPRequestHandler, HTTPServer


def greeting(name):
    return f"Hello, {name}!"


class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        body = greeting("DevOps").encode()
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(body)


if __name__ == "__main__":
    HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()
# Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY app.py .
EXPOSE 8000
USER nobody
CMD ["python", "app.py"]

What each instruction does:

InstructionMeaning
FROMStart from an existing base image (here, a small Debian image with Python 3.12).
WORKDIRSet the working directory inside the image; later commands run there.
COPYCopy files from your project (the build context) into the image.
EXPOSEDocument which port the app listens on. It does not publish the port by itself.
USERRun the app as an unprivileged user instead of root.
CMDThe default command when a container starts.

Notice that the app listens on 0.0.0.0, not 127.0.0.1. Inside a container, 127.0.0.1 means "this container only", so traffic forwarded from your laptop would never reach it.

Build, run, inspect

docker build -t hello-devops:0.1 .
docker run --rm -d --name hello -p 8000:8000 hello-devops:0.1
curl http://localhost:8000        # Hello, DevOps!
docker ps                          # list running containers
docker logs hello                  # the app's output
docker stop hello                  # --rm deletes it once stopped
  • -t hello-devops:0.1 gives the image a name and tag. Use meaningful tags (a version or a Git commit SHA), not only latest.
  • -p 8000:8000 maps port 8000 on your machine to port 8000 in the container (host:container).
  • -d runs it in the background; --rm removes the container when it stops.

Layers and caching

Every instruction in a Dockerfile creates a layer. Docker caches layers and reuses them when nothing they depend on has changed. Order instructions from "changes rarely" to "changes often": install dependencies first, copy your source code last. For a typical Python app that means copying requirements.txt and running pip install before COPY . ., so editing one line of code does not reinstall every library.

Add a .dockerignore file (same syntax as .gitignore) so that .git, virtual environments and, above all, .env files never enter the build context.

Common mistakes

  • Baking secrets into images. Anything you COPY or put in ENV is readable by anyone who can pull the image, even if a later layer deletes it. Pass secrets at run time (environment variables from a secret store) and keep them out of Git.
  • Running as root. Add a USER line; many security scanners flag images that run as root.
  • Huge images. Use -slim or alpine bases and clean up package caches. Smaller images pull faster and have fewer vulnerabilities.
  • Using only latest. You cannot tell which code latest contains, and rolling back becomes guesswork.
  • Forgetting to clean up. Old images and stopped containers fill your disk. docker system df shows usage; docker image prune removes dangling images.

A note on licensing: Docker Desktop is free for personal use, education, open source and small businesses, but larger companies need a paid subscription. On Linux you can install Docker Engine directly, and Podman or Colima are free alternatives. Check Docker's current terms for your situation.

Quick recap

  • An image is a package; a container is a running instance of it.
  • A Dockerfile lists the steps to build an image; each step is a cached layer.
  • docker build, docker run -p, docker ps, docker logs and docker stop cover daily use.
  • Keep secrets out of images and Git, run as a non-root user, and tag images meaningfully.

Try it

Open the Do tab to install Docker, build the image above and run it on your own machine. Everything runs locally and costs nothing.

Also in this lesson: