Skip to Content

Docker Tutorial for Beginners in 2026: Run Your First Container (and What's New)

Docker Tutorial for Beginners in 2026: Run Your First Container (and What's New)

Summary

  • What you'll build: a running web server, your own custom image, and a two-service Compose app.
  • Prerequisites: Docker Desktop installed and running. No prior Docker knowledge needed.
  • Time: about 20 minutes.
  • Level: complete beginner. You can copy-paste every command.
  • What's new: we end with the recent additions worth knowing (docker init, docker scout, and Compose v2).
  • Real output: every command was run on Docker Engine 29.2.0, so the results you see are real, not illustrative.

Everyone on your team says "just containerize it" like it is obvious, and you nod along while quietly wondering where you would even start. The good news is that going from zero to a running container is genuinely a ten-minute job, and once it clicks, a lot of the mystery around Docker disappears.

By the end of this tutorial you will have run your first container, served a real web page from one, built your own image, and run two services together with Docker Compose, all by copy-pasting commands and seeing real output. Every command and every result below was actually run on a clean Docker install, so what you see is what you will get. You need one thing before we start: Docker Desktop installed and running (on Windows, Mac, or Linux). That is the only prerequisite, and the whole walkthrough takes about twenty minutes.

What Is Docker, in One Minute

Docker packages an application together with everything it needs to run (the code, the runtime, the libraries, the settings) into a single unit called an image. When you run that image, you get a container: an isolated process that behaves the same on your laptop, a colleague's machine, and a production server. That is the whole pitch, "it works on my machine" stops being a problem because the machine travels with the app.

An image is the blueprint; a container is a running instance of it. You can start many containers from one image, the same way you open many windows from one program. If you want the deeper background, our Docker for Beginners guide covers the concepts, but you do not need more theory to start, so let us run something.

Before You Start

Confirm Docker is installed and the engine is running. Open a terminal and check the version:

docker --version
Docker version 29.2.0, build 0b9d198

If you get a version number, you are ready. If instead you see an error about not being able to connect to the daemon, Docker Desktop is not running yet, start it and wait for the whale icon to settle (we cover that exact error later). No local install? You can run every command in this tutorial in a browser with KodeKloud's free Docker labs or the Docker playground.

Step 1: Run Your First Container

The traditional first container is hello-world. Run it:

docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
4f55086f7dd0: Pull complete
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

A lot just happened in one command, and it is worth understanding because every other docker run works the same way. Docker looked for the hello-world image locally, did not find it, so it pulled it from Docker Hub (the default public image registry), then created a container from it and ran it. The container printed its message and exited. That pull-then-run flow is the core of Docker.

Step 2: Run a Real Web Server

hello-world runs and quits. Let us run something that stays up: the nginx web server. This command introduces three flags you will use constantly.

docker run -d -p 8080:80 --name myweb nginx

-d runs it detached (in the background), -p 8080:80 maps port 8080 on your machine to port 80 inside the container, and --name myweb gives it a friendly name. Check that it is running:

docker ps
NAMES     IMAGE     STATUS                  PORTS
myweb     nginx     Up Less than a second   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp

Now open http://localhost:8080 in your browser, or curl it, and you will reach the server running inside the container:

curl http://localhost:8080
<title>Welcome to nginx!</title>

That is a real web server, isolated in a container, reachable on your machine because of that port mapping. You can see what it is doing with its logs:

docker logs myweb
192.168.65.1 - - [24/Jun/2026:12:17:34 +0000] "GET / HTTP/1.1" 200 896 "-" "curl/8.7.1" "-"

That line is the request you just made, served with a 200.

Step 3: Look Inside a Running Container

A container is a real (isolated) Linux environment, and you can step into it with docker exec. Here we ask it what operating system it is:

docker exec myweb cat /etc/os-release
PRETTY_NAME="Debian GNU/Linux 13 (trixie)"
NAME="Debian GNU/Linux"

The default official nginx image is built on Debian, which is why your container reports Debian even though your laptop might be macOS or Windows. (There is also a much smaller nginx:alpine variant built on Alpine Linux, which we use in the next step.) You can swap cat /etc/os-release for bash (or sh) to get an interactive shell inside the container, which is how you poke around and debug.

Step 4: Build Your Own Image

Running other people's images is useful, but the real power is packaging your own app. You do that with a Dockerfile, a recipe for building an image. Create a folder, and inside it a file called index.html:

<!doctype html>
<title>My First Docker App</title>
<h1>Hello from my own image!</h1>

Then a file named Dockerfile (no extension) next to it:

FROM nginx:alpine
COPY index.html /usr/share/nginx/html/index.html

FROM nginx:alpine starts from a small nginx image (the Alpine variant is tiny), and COPY drops your page into the folder nginx serves. Build it into an image named myapp:

docker build -t myapp:1.0 .
 => naming to docker.io/library/myapp:1.0
 => unpacking to docker.io/library/myapp:1.0
DONE 0.3s

You now have your own image. Confirm it and check its size:

docker images myapp
REPOSITORY   TAG       SIZE
myapp        1.0       101MB

Run it on a new port and visit it:

docker run -d -p 8081:80 --name myapp myapp:1.0
curl http://localhost:8081
<!doctype html>
<title>My First Docker App</title>
<h1>Hello from my own image!</h1>

That is your code, packaged into a portable image that will run the same way anywhere Docker runs.

Step 5: Run Multiple Services with Compose

Real apps are usually more than one container: a web server, a database, a cache. Docker Compose lets you define them together in one file and run them with a single command. Create compose.yaml:

services:
  web:
    image: nginx:alpine
    ports:
      - "8082:80"
  cache:
    image: redis:alpine

Bring both services up:

docker compose up -d
 Container dockertut-cache-1  Started
 Container dockertut-web-1  Started

Check them:

docker compose ps
SERVICE   IMAGE          STATUS
cache     redis:alpine   Up Less than a second
web       nginx:alpine   Up Less than a second

Two services, one command, on their own private network so they can talk to each other by name. When you are done, tear the whole thing down just as easily:

docker compose down
 Container dockertut-cache-1  Removed
 Container dockertut-web-1  Removed
 Network dockertut_default  Removed

If you want to go deeper on Compose, this KodeKloud walkthrough covers it step by step:

What Just Happened

Step back and the mental model is simple. You pull or build an image (the blueprint), you run it to get a container (a running instance), you map a port to reach it, and you can exec into it to look around. Compose is just a way to declare several of those at once. Everything else in Docker builds on these few ideas. For a full, end-to-end version of this journey, this free KodeKloud tutorial is a great companion:

Going Further: Four Intermediate Skills

The five steps above cover the core loop. These next four skills are what take you from "I can run a container" to "I can run real applications," and they come up constantly in actual work.

Step 6: Persist Data with Volumes

Containers are disposable: when you remove one, anything written inside it is gone. That is a problem for databases and uploads. Volumes solve it by storing data outside the container's own filesystem, so it survives. Create a named volume:

docker volume create mydata

Write a file into it from one container, using -v <volume>:<path> to mount it:

docker run --rm -v mydata:/data alpine sh -c "echo 'persisted across containers' > /data/note.txt"

That container exited and was removed (--rm). Now start a completely different container, mount the same volume, and the data is still there:

docker run --rm -v mydata:/data alpine cat /data/note.txt
persisted across containers

The file outlived the container that wrote it. That is exactly how you keep a database's data safe across restarts and upgrades.

Step 7: Pass Configuration with Environment Variables

Real apps are configured at runtime, not baked into the image. The -e flag sets an environment variable inside the container:

docker run --rm -e GREETING="Hello from an env var" alpine sh -c 'echo $GREETING'
Hello from an env var

This is how you pass things like database URLs, API keys, and feature flags without rebuilding the image. The same app image runs in dev, staging, and production with different environment values.

Step 8: Connect Containers on a Network

Containers usually need to talk to each other (a web app to its database, say). Put them on the same user-defined network and Docker gives you DNS: containers can reach each other by name. Create a network and start a container on it:

docker network create mynet
docker run -d --name api --network mynet nginx

Now run another container on the same network and reach the first one by its name, api, with no IP address needed:

docker run --rm --network mynet alpine sh -c "wget -qO- http://api | grep -i '<title>'"
<title>Welcome to nginx!</title>

The second container resolved api to the nginx container and fetched its page. This name-based discovery is why Compose apps (Step 5) can refer to services by name: Compose puts them on a shared network for you.

Step 9: Build Smaller Images with Multi-Stage Builds

Compiled languages need a full toolchain to build, but not to run. If you ship the toolchain in your image, it is huge. Multi-stage builds fix this: you build in one stage and copy only the finished binary into a small final stage. Here is a tiny Go program (main.go):

package main

import "fmt"

func main() {
	fmt.Println("Hello from a multi-stage build!")
}

A naive single-stage build keeps the whole Go toolchain in the final image:

FROM golang:alpine
WORKDIR /src
COPY main.go .
RUN go mod init app && CGO_ENABLED=0 go build -o app .
CMD ["/src/app"]

The multi-stage version builds in a build stage, then copies just the compiled binary into a clean Alpine image, leaving the compiler behind:

FROM golang:alpine AS build
WORKDIR /src
COPY main.go .
RUN go mod init app && CGO_ENABLED=0 go build -o app .

FROM alpine:latest
COPY --from=build /src/app /app
CMD ["/app"]

The COPY --from=build line is the trick: it reaches into the first stage and takes only the binary. Build both and compare:

docker build -f Dockerfile.single -t goapp:single .
docker build -f Dockerfile.multi  -t goapp:multi  .
docker images | grep goapp
goapp   multi    16.8MB
goapp   single   411MB

Same program, same output, but the multi-stage image is 16.8MB instead of 411MB, because it does not carry the build tools. The slim image still runs exactly the same:

docker run --rm goapp:multi
Hello from a multi-stage build!

Smaller images pull faster, start faster, and have a smaller attack surface, which is why multi-stage builds are standard practice for anything compiled.

Common Errors and Fixes

Beginners hit the same handful of errors. Here they are with the real messages, so you recognize them.

"Cannot connect to the Docker daemon." If a command returns something like this, Docker Desktop is not running:

failed to connect to the docker API at unix:///var/run/docker.sock; check if the path is correct and if the daemon is running

Fix: start Docker Desktop and wait for it to finish starting, then retry.

"Repository does not exist" on docker run. Usually a typo in the image name:

Unable to find image 'nginyx:latest' locally
docker: Error response from daemon: pull access denied for nginyx, repository does not exist or may require 'docker login'

Fix: check the spelling (it is nginx). Docker tried to pull a name that does not exist on the registry.

"Port is already allocated." You tried to map a host port that another container (or app) is already using:

docker: Error response from daemon: failed to set up container networking: ... Bind for 0.0.0.0:8080 failed: port is already allocated

Fix: pick a different host port (for example -p 8090:80), or stop whatever is using 8080.

What's New in Docker

If you learned Docker a few years ago, a few recent additions are worth knowing because they save real time.

  • docker init scaffolds Docker files for you. Run it in a project and it asks a few questions, then generates a sensible Dockerfile, a compose.yaml, and a .dockerignore. It is a great way to start rather than writing them from a blank file.
  • docker scout analyzes an image for known vulnerabilities. docker scout quickview gives a quick overview of an image, and docker scout cves lists the CVEs found, which makes security a first-class part of the build workflow instead of an afterthought.
  • Compose is now built in as docker compose (with a space), replacing the old standalone docker-compose (with a hyphen). If you see older tutorials using the hyphenated command, the modern equivalent is the docker compose subcommand you used above.

These are evergreen habits worth picking up early; they are part of the standard Docker experience now, not add-ons.

Clean Up

Containers and images stick around until you remove them, so tidy up what this tutorial created. Stop and remove the containers, then remove your custom image:

docker rm -f myweb myapp
docker rmi myapp:1.0

docker rm -f stops and removes a container in one step, and docker rmi removes an image. Run docker ps -a to confirm nothing is left running, and you are back to a clean slate.

Next Steps

You can now run containers, build images, and orchestrate a couple of services, which is most of what day-to-day Docker looks like. From here, the natural next steps are learning Dockerfile best practices (smaller images, layer caching, multi-stage builds), volumes for persistent data, and networking between containers. KodeKloud's Docker for the Absolute Beginner course takes you through those with hands-on labs, and the full Docker learning path goes from here to production-grade skills. When you want to validate them, our Docker certification guide covers the options.

Conclusion

Docker stops being intimidating the moment you run something with it, and you just ran several things: a sample container, a real web server, your own image, and a multi-service app. The core loop is small (pull or build an image, run it as a container, map a port, look inside when you need to), and everything else is a variation on it. Keep a terminal open and keep experimenting, because the fastest way to get comfortable is to break things in a container you can throw away.

Ready to Go Beyond Your First Container?

You have the basics; the next step is making them stick with real practice. KodeKloud's Docker for the Absolute Beginner course builds on exactly what you did here with browser-based labs, and the full Docker learning path takes you from first container to production-ready images and Compose setups. The fastest way to learn Docker is to keep running commands, so keep a lab open and keep building.

Start the Docker learning path ->


FAQs

Q1: Do I need to know Linux to use Docker?

It helps, but you do not need to be an expert. Basic command-line comfort is enough to start, and you pick up the Linux details (like why a container reports Debian) as you go. The commands in this tutorial work the same on Windows, Mac, and Linux.

Q2: Is Docker Desktop free?

Docker Desktop is free for personal use, education, non-commercial open source, and small businesses (fewer than 250 employees and under $10 million in annual revenue). Larger organizations need a paid subscription (Pro, Team, or Business). For learning and personal projects like this tutorial, it is free.

Q3: Do I need a cloud account to follow this?

No. Everything here runs locally on your own machine through Docker Desktop. If you cannot install it, a browser-based playground gives you the same environment without any local setup or cloud account.

Q4: What is the difference between an image and a container?

An image is the packaged blueprint (your app plus everything it needs), and a container is a running instance of that image. One image can run as many containers, the way one program can open many windows.

Q5: Is it `docker compose` or `docker-compose`?

Use docker compose (with a space), which is the modern version built into Docker. The older docker-compose (with a hyphen) was a separate tool and is now legacy. If you see the hyphenated form in an old tutorial, the docker compose subcommand is the current equivalent.


Sources: commands and output were run on Docker Engine 29.2.0 with Docker Compose v2 on a clean install. Concepts and the official image behavior follow the Docker documentation (docs.docker.com). KodeKloud learning: Docker for the Absolute Beginner and the Docker learning path.

Nimesha Jinarajadasa Nimesha Jinarajadasa
Nimesha Jianrajadasa is a DevOps & Cloud Consultant, K8s expert, and instructional content strategist-crafting hands-on learning experiences in DevOps, Kubernetes, and platform engineering.

Subscribe to Newsletter

Join me on this exciting journey as we explore the boundless world of web design together.