Docker Basics (Images & Containers)
Once Docker is installed, you spend most of your time with a handful of commands: start a container, see what’s running, look at its logs, jump inside it for a shell, and clean it up. This page is your hands-on tour of those everyday commands. By the end you’ll have run a real web server (Nginx) inside a container and opened it in your browser — all without installing Nginx on your Ubuntu machine.
A quick vocabulary refresher. An image is a read-only template — a packaged snapshot of an app plus everything it needs (think of it as a recipe). A container is a running copy made from that image (the cooked dish). You can start many containers from one image. Everything below uses the Docker CLI (the docker command-line tool) on Ubuntu 22.04 / 24.04 LTS.
Tip: If you have to type
sudo dockerfor every command, you haven’t added your user to thedockergroup yet. Runsudo usermod -aG docker $USER, then log out and back in. See the installing Docker page. The examples below assume that’s done, so they drop thesudo.
Running your first container
The core command is docker run. It pulls the image if you don’t have it, creates a container from it, and starts it. Let’s run Nginx — a popular web server — and expose it on your machine.
docker run -d -p 8080:80 --name web nginx
What each flag means:
| Flag | Meaning | When to use it |
|---|---|---|
-d | Detached — run in the background and return your prompt | Almost always for servers (web apps, databases) |
-p 8080:80 | Publish port — map host port 8080 to container port 80 | When the app inside listens on a port you want to reach |
--name web | Give the container a friendly name | So you don’t have to use random IDs later |
-e KEY=value | Set an environment variable inside the container | Passing config/secrets (DB passwords, API keys) |
Output:
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
a2318d6c47ec: Pull complete
Status: Downloaded newer image for nginx:latest
3f9a8b1c7d2e5a04c1b9e2f3a4d5c6b7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3
That long string is the container ID. Now hit the server. Inside the container Nginx listens on port 80; we mapped that to 8080 on the host, so:
curl http://localhost:8080
Output:
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
</html>
You can also open http://localhost:8080 in a browser and see the “Welcome to nginx!” page. You just ran a web server with zero installation.
Port mapping explained
The format is -p HOST:CONTAINER. The container always uses its own internal port (Nginx uses 80); the host port is whatever you pick. Use a high port like 8080 during testing so you don’t clash with anything already on 80. When NOT to map a port: if a container only talks to other containers (e.g. a database that only your app uses), leave -p off and connect them over a Docker network instead — exposing a database to the host is a needless security risk.
Passing environment variables
Many images are configured through environment variables. For example, the official Postgres image needs a password:
docker run -d -p 5432:5432 --name db -e POSTGRES_PASSWORD=secret postgres
You can repeat -e as many times as you need. This is the standard way to inject config without baking secrets into an image.
Seeing what’s running
docker ps lists running containers.
docker ps
Output:
CONTAINER ID IMAGE COMMAND STATUS PORTS NAMES
3f9a8b1c7d2e nginx "/docker-entrypoint.…" Up 2 minutes 0.0.0.0:8080->80/tcp web
To include stopped containers too, add -a:
docker ps -a
Logs and getting a shell inside
When something misbehaves, the first place to look is the container’s logs. docker logs shows whatever the app printed to stdout/stderr.
docker logs web
Output:
172.17.0.1 - - [15/Jun/2026:10:22:31 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.5.0"
Add -f to follow the logs live (like tail -f), and Ctrl+C to stop following:
docker logs -f web
To poke around inside a running container, open a shell with docker exec. The -it part means interactive + tty (so you get a real interactive terminal).
docker exec -it web bash
Output:
root@3f9a8b1c7d2e:/#
You’re now inside the container as root. Try ls /usr/share/nginx/html to see the files Nginx serves, then type exit to return to your host. When to use this: debugging — checking config files, env vars, or network reachability from inside the container. When NOT to: don’t make permanent changes this way; they vanish when the container is recreated. Real changes belong in a Dockerfile.
Gotcha: Minimal images (like
alpine-based ones) don’t shipbash. Ifdocker exec -it web bashfails with “executable file not found”, useshinstead:docker exec -it web sh.
Working with images
List the images you’ve downloaded with docker images:
docker images
Output:
REPOSITORY TAG IMAGE ID CREATED SIZE
nginx latest a2318d6c47ec 2 weeks ago 188MB
postgres latest c1b9e2f3a4d5 3 weeks ago 438MB
To download an image ahead of time without running it, use docker pull. Always pin a specific tag (version label) in production rather than the moving latest:
docker pull nginx:1.27
Stopping and cleaning up
docker stop gracefully stops a container; docker rm deletes it. You must stop a container before removing it (or force it with -f).
docker stop web
docker rm web
To stop and remove in one go:
docker rm -f web
To remove an image you no longer need (no containers may be using it):
docker rmi nginx
A quick reference for the lifecycle:
| Command | What it does |
|---|---|
docker run | Create + start a container from an image |
docker stop <name> | Gracefully stop a running container |
docker start <name> | Restart a stopped container (keeps its state) |
docker rm <name> | Delete a stopped container |
docker rmi <image> | Delete an image |
docker ps -a | List all containers (running and stopped) |
Best practices
- Always give containers a clear
--nameso commands read like English and scripts don’t depend on random IDs. - Pin image tags (e.g.
nginx:1.27) instead oflatestso deployments are reproducible. - Use
-dfor long-running services, and never publish a port with-punless something outside the container actually needs to reach it. - Pass configuration and secrets through
-eenvironment variables, not by editing files inside a running container. - Check
docker logs <name>first when a container won’t start or behaves oddly — most failures print a clear reason there. - Clean up regularly with
docker rm -fanddocker rmi; stopped containers and old images quietly eat disk space (docker system pruneclears the lot when you’re sure).