Deployn

Docker for Beginners: Install It, Run Containers, Understand Images

Learn Docker step by step: images and containers explained, installation on Debian and Ubuntu, your first web server, managing containers and updating Docker.

Published on
Docker for Beginners: Install It, Run Containers, Understand Images hero image
Contents
Tested with
Docker 29.9Docker Compose 5.7nginx 1.30Debian 13Ubuntu 26.04
Last checked on

You got a program running on your own computer, copied it to a server, and suddenly nothing works. A library is missing, the version is wrong, or another program already uses the same port. Docker solves this by packing a program together with everything it needs into one package. That package starts the same way on every machine that has Docker.

In this post we install Docker on a Linux server and use it to start a web server, step by step.

What you need

  • A Linux server, ideally running Debian or Ubuntu. Whether it is a cloud server, a Raspberry Pi or a computer on your home network does not matter for the Docker commands. Below I use servers running Debian 13 and Ubuntu 26.04. How to pick and secure a server is covered in my guide VPS for beginners. If you create it at Hetzner, the Cloud-Init generator helps you set up the user.
  • A connection via SSH. This is the way your computer sends commands to the server. The VPS guide covers that as well.
  • A user who may use sudo. Below, the user is called deployn. If you are logged in as root, leave out the word sudo in all commands.

You copy the commands in the grey boxes into the terminal and confirm with Enter. Characters like $ at the start of a line are not part of the command, and I do not write them. If the terminal still feels unfamiliar, read the most important Linux commands first.

Connect to your server now:

Terminalbash
ssh {{username}}@{{server_ip}}

You can tell it worked from the prompt. It starts with deployn@ and the name of your server.

The most important terms

Before we install anything, let us clarify a few words that appear in every Docker guide. The order is: a blueprint becomes a running application.

Term Meaning
Image A finished, unchangeable package made of a program and everything it needs. The image is the blueprint.
Container A running program that was started from an image. You can start as many containers from one image as you like.
Registry A storage place for images. The best known is Docker Hub. Docker fetches images from there when they are not yet on your server.
Dockerfile A text file that describes how an image is built. You only need it once you create your own images. It does not appear in this post.

The flow in one sentence: Docker fetches an image from a registry and starts a container from it.

Is a container a virtual machine?

No. A virtual machine brings a complete operating system of its own. A container shares the kernel of the operating system with your server and is only walled off from the other programs. Think of an apartment building: every flat has its own doors and furniture, but all share the foundation and the heating. That is why containers start in seconds and need less memory than virtual machines.

Installing Docker

Debian offers Docker in its own package archive, but the version there is not current. So we use the package source that Docker maintains itself. It consists of five small steps.

Step 1: Remove old Docker packages

On a freshly created server you have nothing to remove. The command does no harm anyway. It only removes packages that would clash with Docker from the official source:

bash

Debian

sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-doc docker-buildx podman-docker containerd runc | cut -f1)

Ubuntu

sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc | cut -f1)

Messages like dpkg: no packages found matching docker-compose only mean that this package was not installed. Containers and images that already exist on the server stay in place.

Step 2: Store Docker’s key

Linux checks installed packages with a digital key, so nobody can slip you modified software. We fetch this key now. Choose the tab for your system:

bash

Debian

sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

Ubuntu

sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

Step 3: Add the package source

Now you tell the system where to find the Docker packages. Copy the whole block, including the last line EOF:

bash

Debian

sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/debian
Suites: $(. /etc/os-release && echo "$VERSION_CODENAME")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

Ubuntu

sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

The command writes a small text file and prints its content once more. The line Suites: contains the name of your version, for example trixie on Debian 13 and resolute on Ubuntu 26.04.

Terminalbash
sudo apt update

The output now contains a line with download.docker.com. That means your server knows the new source.

Step 4: Install Docker

Terminalbash
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

These are several packages in one command: the Docker Engine (docker-ce), the command line program (docker-ce-cli), a helper component (containerd.io) and two extensions. One of them, docker-compose-plugin, you will need later for Docker Compose.

Debian shows a list of packages and asks Do you want to continue? [Y/n]. Press Enter to confirm. The capital Y shows that “yes” is the default. The installation takes one to two minutes.

Step 5: Check the installation

Terminalbash
sudo docker --version
sudo docker compose version
Docker version 29.9.3, build 5457199
Docker Compose version v5.7.0

Your numbers may be higher. What matters is that versions appear at all. You can also check that the Docker service runs in the background and starts automatically when the server boots:

Terminalbash
systemctl is-active docker
systemctl is-enabled docker
active
enabled

If something else appears here, start the service with sudo systemctl start docker and switch on autostart with sudo systemctl enable docker.

Why does every command start with sudo?

Docker reaches deep into the system and needs administrator rights for that. Without sudo you get this message:

permission denied while trying to connect to the docker API at unix:///var/run/docker.sock

You can add your user to the group docker, and then sudo is no longer needed. I do not recommend it. A user in this group may start a container that mounts folders of the server. If it mounts the server’s /etc folder, for example, the container can see the file /etc/shadow, which holds the password hashes. The user is not allowed to read it on the server itself (cat /etc/shadow reports Permission denied).

Terminalbash
docker run --rm -v /etc:/host alpine ls -l /host/shadow
-rw-r-----    1 root     shadow         634 Nov  7 06:17 /host/shadow

So the group docker is practically the same as administrator rights. I stay with sudo here.

The first container: hello-world

Now we start the first container. The image hello-world is a tiny test that only prints a text:

Terminalbash
sudo docker run hello-world

This is what happens on your server:

  1. Docker looks for the image hello-world on the server and does not find it. You see the message Unable to find image 'hello-world:latest' locally.
  2. Docker downloads the image from Docker Hub (Pulling and Pull complete).
  3. Docker starts a container from it. It prints a text that begins with Hello from Docker!.
  4. The container has nothing more to do and ends by itself.

If you see the text Hello from Docker!, your installation works. The container still exists, it just no longer runs. You will see that shortly when we manage containers.

The second container: a web server

That was a test. Now we start something you can look at in the browser. nginx is a web server, a program that delivers web pages.

Terminalbash
sudo docker run nginx

Docker downloads the image nginx and starts the container. Some lines scroll by, and the prompt does not come back:

/docker-entrypoint.sh: Configuration complete; ready for start up

This is intended. A web server has no task that ends at some point; it keeps running and waits for visitors. The terminal shows you its output, which is why it is blocked. Press Ctrl + C to stop the container.

Three problems remain. The terminal is blocked. You cannot reach the web server from the browser, because it is walled off from the outside world. And you do not know which nginx version is running. We improve the command step by step.

The complete command

Terminalbash
sudo docker run -d -p 8080:80 --name mein-webserver nginx:1.30

It looks longer, but it consists of only five parts:

Part Meaning
docker run Start a new container.
-d “Detached”: the container runs in the background and the terminal stays free.
-p 8080:80 Forward requests that arrive at port 8080 of your server to port 80 in the container. More on this in a moment.
--name mein-webserver Give the container a name. Without a name, Docker picks a random one like musing_elbakyan.
nginx:1.30 The image: the name on the left of the colon, the tag on the right. The tag sets the version.

The first time, the output is long because the image gets downloaded. At the end there is a long string of characters, the ID of the new container.

What does 8080:80 mean?

A computer has many ports, numbered entrances through which programs accept requests. By default, the browser sends requests for normal web pages to port 80 (HTTP) and 443 (HTTPS). The nginx web server in the container waits on port 80. That port only exists inside the container, and nobody from outside can reach it.

With -p 8080:80 you build a connection: whatever arrives at port 8080 on the server ends up at port 80 in the container. The number on the left belongs to the server, the number on the right to the container. The right number is fixed by nginx. You can choose the left one freely as long as the port is still free.

Look at it in the browser

Open this address in your browser:

http://{{server_ip}}:8080

You see the page “Welcome to nginx!”. You have just started a web server without installing it.

To check on the server itself:

Terminalbash
curl -s localhost:8080 | head -n 8
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

Several web servers side by side

Because every container is walled off, you can run several at the same time, even in different versions. Start two more:

Terminalbash
sudo docker run -d -p 8081:80 --name mein-webserver-2 nginx:1.30
sudo docker run -d -p 8082:80 --name mein-webserver-3 nginx:1.28

Now three web servers run, reachable at ports 8080, 8081 and 8082, two of them in version 1.30 and one in version 1.28. Without Docker you would have to keep three installations apart.

What happens when you make a mistake? Repeat the first command with a different name but the same port:

Terminalbash
sudo docker run -d -p 8080:80 --name zweiter nginx:1.30
docker: Error response from daemon: ... Bind for 0.0.0.0:8080 failed: port is already allocated

Port 8080 is already taken. Choose another one. And when you use a name twice:

docker: Error response from daemon: Conflict. The container name "/mein-webserver" is already in use by container "...". You have to remove (or rename) that container to be able to reuse that name.

Every name may only appear once. Note also that the failed start still leaves a container with the status Created behind. It shows up in the list (sudo docker ps -a), and you remove it with sudo docker rm zweiter. How that works is shown in the next section.

Managing containers

A web server is only useful once you can also stop it, inspect it and delete it. Seven commands cover that.

Show running containers

Terminalbash
sudo docker ps
CONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS         PORTS                                     NAMES
e8e360e0586c   nginx:1.28   "/docker-entrypoint.…"   2 seconds ago   Up 2 seconds   0.0.0.0:8082->80/tcp, [::]:8082->80/tcp   mein-webserver-3
276fdfb7fef4   nginx:1.30   "/docker-entrypoint.…"   6 seconds ago   Up 6 seconds   0.0.0.0:8081->80/tcp, [::]:8081->80/tcp   mein-webserver-2
758f8ae64784   nginx:1.30   "/docker-entrypoint.…"   6 seconds ago   Up 6 seconds   0.0.0.0:8080->80/tcp, [::]:8080->80/tcp   mein-webserver

Every line is a container. Four columns matter for you: the ID (CONTAINER ID), the image, the status (Up means: running) and the name on the far right. 0.0.0.0:8080->80/tcp means that port 8080 of the server points to port 80 of the container. The second entry with [::] is the same forwarding for IPv6, the newer form of internet addresses.

A command with the same effect is sudo docker container ls. It is longer; docker ps is shorter and more common.

Only the IDs, without a table, come from the flag -q (“quiet”):

Terminalbash
sudo docker ps -q

Which flags a command has is shown by --help, for example sudo docker ps --help.

Stop and restart containers

You stop a container by its name:

Terminalbash
sudo docker stop mein-webserver-3

Docker prints the name as confirmation. The page on port 8082 no longer loads. Instead of the name you can also use the ID, and the first few characters are enough as long as they are unique, so for instance e8e.

The container is not gone. docker ps only shows running containers; with -a (“all”) you also see the stopped ones:

Terminalbash
sudo docker ps -a
CONTAINER ID   IMAGE        COMMAND                  CREATED         STATUS                              PORTS                                     NAMES
e8e360e0586c   nginx:1.28   "/docker-entrypoint.…"   2 seconds ago   Exited (0) Less than a second ago                                             mein-webserver-3
276fdfb7fef4   nginx:1.30   "/docker-entrypoint.…"   6 seconds ago   Up 6 seconds                        0.0.0.0:8081->80/tcp, [::]:8081->80/tcp   mein-webserver-2
758f8ae64784   nginx:1.30   "/docker-entrypoint.…"   6 seconds ago   Up 6 seconds                        0.0.0.0:8080->80/tcp, [::]:8080->80/tcp   mein-webserver

On your server this list also contains the hello-world container and the first nginx container with their random names, both with the status Exited. You start a stopped container again with:

Terminalbash
sudo docker start mein-webserver-3

Remove containers

With docker rm you delete a container for good:

Terminalbash
sudo docker rm mein-webserver-3

A running container cannot be deleted. Docker then reports cannot remove container "mein-webserver-3": container is running: stop the container before removing or force remove. First stop, then rm. The same in one step is sudo docker rm -f mein-webserver-3.

You remove all stopped containers at once with:

Terminalbash
sudo docker container prune

Docker asks first (Are you sure you want to continue? [y/N]). Here N is the default. So you have to type y; a bare Enter cancels.

What happens to files in a container when you delete it

This is the most important property of containers and a frequent beginner mistake. Try it out. Create a file in the running container mein-webserver:

Terminalbash
sudo docker exec mein-webserver sh -c "echo hallo > /usr/share/nginx/html/test.html"
curl -s localhost:8080/test.html
hallo

docker exec runs a command in a running container. Here it creates a file that nginx serves right away. Stopping and starting again changes nothing; the file is still there afterwards. Now we delete the container and start a new one from the same image:

Terminalbash
sudo docker stop mein-webserver
sudo docker rm mein-webserver
sudo docker run -d -p 8080:80 --name mein-webserver nginx:1.30
curl -s -o /dev/null -w "%{http_code}\n" localhost:8080/test.html
404

The status code 404 means “not found”. The file is gone. A container is like a disposable product that can be recreated from the image at any time. Everything you change inside it is lost when it is removed. Programs that must keep data, such as databases or photo collections, therefore store it outside the container. For that there are volumes and bind mounts, a topic of its own.

Read the logs

When something does not work, you look at the logs first. These are the messages a program writes about its work:

Terminalbash
sudo docker logs --tail 2 mein-webserver
2026/11/07 06:25:27 [error] 30#30: *1 open() "/usr/share/nginx/html/test.html" failed (2: No such file or directory), client: 172.17.0.1, server: localhost, request: "GET /test.html HTTP/1.1", host: "localhost:8080"
172.17.0.1 - - [07/Nov/2026:06:25:27 +0000] "GET /test.html HTTP/1.1" 404 153 "-" "curl/8.14.1" "-"

You can see that the request for /test.html was answered with 404, and the reason: No such file or directory. --tail 2 shows only the last two lines. With -f (“follow”) the terminal stays open and shows new entries as they arrive. You end that with Ctrl + C.

Look inside a container

With exec and a shell you open a terminal inside the container:

Terminalbash
sudo docker exec -it mein-webserver bash

-i keeps the input open, -t provides a proper terminal. The prompt changes:

root@06fa4833f3b5:/#

You are now inside the container. The name after the @ is the container ID. Here you can run ls /usr/share/nginx/html and you see 50x.html and index.html. With exit you return to the server.

Some images have no shell called bash. Then you get exec: "bash": executable file not found in $PATH, and you try sh instead. This affects especially slim images, as you will see in the next section.

Images and tags

At the start, Docker downloaded the image and stored it. This list shows what lies on your server:

Terminalbash
sudo docker image ls
IMAGE        ID             DISK USAGE   CONTENT SIZE   EXTRA
nginx:1.28   fda7399e8104        161MB             0B   U
nginx:1.30   53d8f012a1a7        163MB             0B   U

The part before the colon is the name, the part after it is the tag. The tag sets which version you get. The U in the last column means that a container is currently using this image.

Two images with the same name but a different tag are different images. The image ID tells them apart. With Docker, the same ID means the same content: if two tags had the same ID, they would take up the disk space only once.

Why you should avoid latest

If you leave out the tag, Docker adds latest. So sudo docker run nginx is the same as sudo docker run nginx:latest. The name sounds like “the newest version”, but it is just a tag like any other. The author of the image decides what it points to and can switch it to a new version at any time.

At the time of this post, nginx:latest pointed to nginx 1.31.8, while nginx:1.30 delivered exactly version 1.30. If you use latest and recreate your container, you eventually get a different version than before, possibly with different settings. That is why I specify a fixed tag in every project. Then I know what runs.

Reading version numbers

Most programs number their versions in the pattern Major.Minor.Patch, for example 1.30.2. This is called semantic versioning. It helps you estimate how risky an update is:

What changes Example Meaning
The last number 1.30.1 → 1.30.2 Bug fix. The update is usually unproblematic.
The middle number 1.30.2 → 1.31.0 New features that should still support old ones.
The first number 1.31.0 → 2.0.0 Big change. Existing settings may stop working afterwards.

With Docker images, a tag can be coarse or fine. nginx:1.30 keeps you on version 1.30 and picks up bug fixes. nginx:1 also follows bigger jumps within version 1. At the time of my test, this tag delivered nginx 1.31.8. Which tags exist and what they mean is decided by each project. You find an overview in the “Tags” tab of the image on Docker Hub.

The alpine tag

On Docker Hub there are many images with an addition in the tag, such as nginx:1.30-alpine. Alpine is a Linux trimmed for small size. Download the image and compare:

Terminalbash
sudo docker pull nginx:1.30-alpine
sudo docker image ls nginx
IMAGE               ID             DISK USAGE   CONTENT SIZE   EXTRA
nginx:1.28          fda7399e8104        161MB             0B   U
nginx:1.30          53d8f012a1a7        163MB             0B   U
nginx:1.30-alpine   1390db6b01a8       62.4MB             0B

The Alpine variant takes only about a third (62.4 MB against 163 MB). Smaller images download faster, take less space and contain fewer programs that could have security holes. The price is that some things are missing. Start a container from the Alpine image and try to look inside:

Terminalbash
sudo docker run -d --name alp nginx:1.30-alpine
sudo docker exec alp bash
OCI runtime exec failed: exec failed: unable to start container process: exec: "bash": executable file not found in $PATH

The image has no bash. It works with sh:

Terminalbash
sudo docker exec -it alp sh

Remove this test container afterwards with sudo docker rm -f alp.

Delete images

You delete an image with docker image rm. As long as a container uses it, Docker refuses:

Error response from daemon: conflict: unable to remove repository reference "nginx:1.30-alpine" (must force) - container 6bf29c9edd00 is using its referenced image 1390db6b01a8

Remove the container first, then it works:

Terminalbash
sudo docker rm -f alp
sudo docker image rm nginx:1.30-alpine

Cleaning up

At the end we remove everything we started in this post. How much space Docker uses on your server is shown by:

Terminalbash
sudo docker system df
TYPE            TOTAL     ACTIVE    SIZE      RECLAIMABLE
Images          2         2         323.4MB   0B (0%)
Containers      3         3         3.285kB   0B (0%)
Local Volumes   0         0         0B        0B
Build Cache     0         0         0B        0B

Remove the containers, and then all images that no container uses any more:

Terminalbash
sudo docker rm -f mein-webserver mein-webserver-2 mein-webserver-3
sudo docker image prune -a

image prune -a asks (y/N) and then deletes every image without a container. That is handy for cleaning up. On a server with running applications, first check briefly that all of them are running, otherwise images get deleted that you have to download again later.

Updating Docker

You do not have to uninstall Docker to update it. Docker comes from a package source, and you update it like everything else on Debian:

Terminalbash
sudo apt update
sudo apt install --only-upgrade docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Alternatively, sudo apt upgrade updates the whole system and Docker with it.

When Docker updates, the Docker service restarts, and the containers are stopped in the process. Containers you started with --restart unless-stopped come back up on their own. All others stay at Exited until you start them with docker start. After a restart of the Docker service (sudo systemctl restart docker), containers without this setting are at Exited. So do not schedule updates for a moment when someone is using your service. The data itself lies in /var/lib/docker and stays untouched. If your data matters to you, back it up beforehand anyway. sudo docker info shows the folder /var/lib/docker as “Docker Root Dir”.

The Docker update does not replace the images of your containers. A container keeps running with the image it was started from. For a newer version of a program you fetch the new image (sudo docker pull nginx:1.30 downloads bug fixes if there are any) and recreate the container. With Docker Compose that takes two commands.

Common errors

Message Cause and solution
docker: command not found Docker is not installed, or docker.io was installed. Follow the steps above.
permission denied while trying to connect to the docker API You forgot sudo.
port is already allocated The port left of : is already in use. Choose another or remove the container that holds it.
The container name ... is already in use The name is taken. Choose another or remove the old container with docker rm.
cannot remove container ...: container is running First docker stop, then docker rm, or docker rm -f.
exec: "bash": executable file not found in $PATH The image has no bash. Use sh.
The page does not load in the browser Check with sudo docker ps whether the container runs and which port is entered. Then sudo docker logs.

Conclusion

Six commands already take you far: docker run starts a container, docker ps shows what runs, docker logs tells you what goes wrong, docker stop and docker rm stop and remove it, and docker image ls shows the downloaded images. Three ideas matter: containers are created from images, files in a container disappear with it, and fixed tags instead of latest keep your installation reproducible.

In the long run you will not want to type such commands by hand every time. That is what Docker Compose is for: you write all settings into one file once. You can see Compose in use in my guides on Uptime Kuma or Vaultwarden.

Which step took you the longest? Tell me in the comments and I will extend that part.

FAQ

Frequently asked questions

What is the difference between an image and a container?

An image is the template: a finished package of program and accessories that does not change. A container is a running program that was started from the image. You can start as many containers from one image as you like.

Is a Docker container a virtual machine?

No. A virtual machine brings a complete operating system. A container shares the kernel with the server and is only walled off from other programs. That is why it starts faster and needs less memory.

Why shouldn't I install Docker with apt install docker.io?

The Debian package docker.io lags several major versions behind the current one. On Debian 13 it is version 26.1.5, while the official version is 29.9.3.

How do I update Docker without losing data?

With sudo apt update and sudo apt install --only-upgrade docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin. Uninstalling is not necessary. Containers and volumes are kept, but the containers are restarted briefly during the update.

Is Docker Desktop free?

For individuals, education, open source projects and small businesses, yes. According to Docker the limit is fewer than 250 employees and less than 10 million US dollars in annual revenue. The Docker Engine you install on a Linux server is not affected by this.

Why does my container not run although the command showed no error?

Check the status with sudo docker ps -a and the cause with sudo docker logs NAME. If the container shows Exited, the program in the container has ended. The reason is in the logs.

Do I always have to type y when asked?

Not with apt. At the prompt [Y/n], yes is the default (capital Y), and Enter is enough. For docker container prune or docker image prune -a the prompt reads [y/N]: there, no is the default and you have to type y.