Skip to content
DevOps devops containers 5 min read

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 docker for every command, you haven’t added your user to the docker group yet. Run sudo 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 the sudo.

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:

FlagMeaningWhen to use it
-dDetached — run in the background and return your promptAlmost always for servers (web apps, databases)
-p 8080:80Publish port — map host port 8080 to container port 80When the app inside listens on a port you want to reach
--name webGive the container a friendly nameSo you don’t have to use random IDs later
-e KEY=valueSet an environment variable inside the containerPassing 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 ship bash. If docker exec -it web bash fails with “executable file not found”, use sh instead: 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:

CommandWhat it does
docker runCreate + 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 -aList all containers (running and stopped)

Best practices

  • Always give containers a clear --name so commands read like English and scripts don’t depend on random IDs.
  • Pin image tags (e.g. nginx:1.27) instead of latest so deployments are reproducible.
  • Use -d for long-running services, and never publish a port with -p unless something outside the container actually needs to reach it.
  • Pass configuration and secrets through -e environment 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 -f and docker rmi; stopped containers and old images quietly eat disk space (docker system prune clears the lot when you’re sure).
Last updated June 15, 2026
Was this helpful?