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 initscaffolds Docker files for you. Run it in a project and it asks a few questions, then generates a sensible Dockerfile, acompose.yaml, and a.dockerignore. It is a great way to start rather than writing them from a blank file.docker scoutanalyzes an image for known vulnerabilities.docker scout quickviewgives a quick overview of an image, anddocker scout cveslists 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 standalonedocker-compose(with a hyphen). If you see older tutorials using the hyphenated command, the modern equivalent is thedocker composesubcommand 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.
Discussion