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

URL: https://www.devopsness.com/blog/docker-beginners-tutorial-first-container
Author: Kiril Urbonas
Published: 2026-04-29
Updated: 2026-05-14
Categories: DevOps
Tags: Docker, Containers, DevOps, Tutorial, Beginner

Summary: 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.

---
# Docker for Beginners: Build, Run, and Ship Your First Container

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
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
mkdir docker-tutorial && cd docker-tutorial
```

`app.py`:

```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
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
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
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:

```
[+] 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
docker images | head -3
```

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

## Step 5: Run it

```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
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
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
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.

## What to read next

You've got the bones. The next levels:

- [Docker multi-stage builds for production](/blog/docker-multi-stage-builds-production) — how to make images smaller, faster, more secure
- [Docker for Beginners: Build, Run, and Ship Your First Container — image hardening](/blog/best-practices-docker-image-hardening-for-production) — the production checklist for a Dockerfile that doesn't bite you in six months
- [Linux container internals](/blog/linux-container-internals-understanding-how-containers-work) — what's actually under the hood when you run `docker run`

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

