Skip to main content
Docker for Beginners — Build, Run, and Ship Your First Container

Docker for Beginners — Build, Run, and Ship Your First Container

KU
• 5 min read•Updated •39 views

Walk through your first Dockerfile, container run, and image push in 30 minutes. No theory dumps — just the commands and what each one is doing.

By the end of this post you'll have built a container image from a tiny Python web app, run it locally, and pushed it to Docker Hub. You'll understand what each command actually does — not just the magic incantations.

You'll need Docker installed (Docker Desktop on Mac/Windows, or docker on Linux), a free Docker Hub account, and about 30 minutes.

What a container actually is

A container is a process — a regular Linux process — running with extra kernel features that make it think it's alone on the machine. Its filesystem is isolated, its network is its own, its process tree starts fresh at PID 1. But it shares the host kernel; there's no second OS booting up.

That's why containers start in milliseconds while VMs take minutes. They're not little operating systems. They're regular processes with isolation features applied.

A "Docker image" is a packaged filesystem + metadata that tells Docker how to start the container. You build images from a Dockerfile. You run them as containers. You ship them by pushing to a registry.

That's the whole model. Let's run it.

Step 1: Verify Docker works

bash.bash
docker --version
docker run hello-world

You should see Docker print version info, then pull and run the hello-world test image, which prints a friendly message confirming everything works. If this fails, fix Docker before moving on — nothing else will work.

Step 2: Write a tiny app

Make a new directory and create two files:

bash.bash
mkdir docker-tutorial && cd docker-tutorial

app.py:

python.python
from http.server import BaseHTTPRequestHandler, HTTPServer
import os

class Handler(BaseHTTPRequestHandler):
    def do_GET(self):
        self.send_response(200)
        self.send_header("Content-Type", "text/plain")
        self.end_headers()
        self.wfile.write(f"Hello from {os.uname().nodename}\n".encode())

if __name__ == "__main__":
    HTTPServer(("0.0.0.0", 8000), Handler).serve_forever()

That's a minimal HTTP server. No framework, no dependencies — pure stdlib. It listens on port 8000 and responds with the hostname.

Test it locally first:

bash.bash
python3 app.py &
curl http://localhost:8000
kill %1

You should see something like Hello from your-laptop-name. If that works, the app is fine — any issues from here are container-related, not code.

Step 3: Write the Dockerfile

Create Dockerfile in the same directory:

dockerfile.dockerfile
FROM python:3.12-slim

WORKDIR /app

COPY app.py .

EXPOSE 8000

CMD ["python", "app.py"]

Each instruction:

  • FROM python:3.12-slim — start from an official Python image. slim means without extra tooling we don't need.
  • WORKDIR /app — set the working directory inside the container. Subsequent commands run relative to here.
  • COPY app.py . — copy the local app.py into /app/app.py inside the image.
  • EXPOSE 8000 — document that the container listens on port 8000. (This doesn't actually publish the port — it's just metadata.)
  • CMD ["python", "app.py"] — what to run when the container starts.

That's a complete Dockerfile. No tricks.

Step 4: Build the image

bash.bash
docker build -t hello:1 .

The -t hello:1 tags the image as hello with tag 1. The . is the build context — the directory whose files Docker can copy into the image.

You'll see output like:

code
[+] Building 4.2s
 => [internal] load build definition from Dockerfile
 => [1/2] FROM docker.io/library/python:3.12-slim
 => [2/2] COPY app.py .
 => exporting to image
 => => writing image sha256:abc123...
 => => naming to docker.io/library/hello:1

Confirm:

bash.bash
docker images | head -3

You should see hello 1 abc123... 45 seconds ago 125MB.

Step 5: Run it

bash.bash
docker run --rm -p 8000:8000 --name hello hello:1

Flag breakdown:

  • --rm — delete the container when it stops (avoids leaving dead containers around)
  • -p 8000:8000 — publish container port 8000 to host port 8000 (now accessible from your laptop)
  • --name hello — give it a friendly name we can refer to
  • hello:1 — the image to run

In a second terminal:

bash.bash
curl http://localhost:8000

You should see Hello from <some random hex string>. That string is the container's hostname — different from your laptop, because the container has its own UTS namespace. That's isolation in action.

Stop it with Ctrl-C in the first terminal.

Step 6: Push it

Sign up for a free Docker Hub account if you don't have one. Then:

bash.bash
docker login
docker tag hello:1 yourusername/hello:1
docker push yourusername/hello:1

Replace yourusername with your Docker Hub username. After the push completes, anyone (including you, on another machine) can pull and run it:

bash.bash
docker run --rm -p 8000:8000 yourusername/hello:1

That's the whole loop: build, run, ship. Everything else in Docker is variations and optimizations on those three steps.

Common mistakes

Forgetting the port mapping. docker run hello:1 (without -p) starts the container but doesn't expose its port. curl localhost:8000 will fail. Always remember -p host:container.

Building with no tag. docker build . (without -t) creates an image with no name. You can run it by ID but it's annoying. Always tag.

Editing files inside the running container. Anything you do inside a container is gone when the container stops (unless you mounted a volume). Edit on the host, rebuild, re-run.

Using :latest tag for everything. latest is the default but unspecific. When you push v2, anyone running :latest silently gets the new version. Use real version tags (:1, :1.2.3, :abc123 for git SHAs) in production.

You've got the bones. The next levels:

Docker isn't a complicated technology. Once you've shipped one image you've used most of the surface area. The rest is patterns.

Explore topics:DockerDevOps
React
Share this post
KU

Kiril Urbonas

AI Engineer

560 articles
View all articles by Kiril Urbonas

You might have missed

Evergreen posts worth revisiting.