0%
Installation and First Steps

Installation

A running engine, and the commands that really matter.

10-15 min

Installation and First Steps

Installing Docker takes five minutes when you follow the right procedure, and an entire evening when you follow the wrong one. The most common trap is the docker.io or docker package offered by your distribution’s package manager. It exists, it installs, it more or less works — and it is often several major versions behind, sometimes without the Compose plugin, which will have you searching for twenty minutes to find out why a command from the official documentation “doesn’t exist” on your machine.

The second trap lies elsewhere, and almost everyone hits it once: after a perfectly successful installation, the first command responds with permission denied while trying to connect to the Docker daemon socket. This is not an installation failure. It is a direct consequence of the architecture covered in the previous lesson — the client talks to a privileged daemon through a socket — and it is fixed with a single line, as long as you understand which one and why.

This lesson gets you past both obstacles for your system, then focuses on what really matters: about ten commands that cover 90% of daily work. Not an exhaustive list of options — the documentation exists for that — but the small set you will type every day, along with what happens behind each one.

By the end, you will know how to start a container, get inside it, read its logs, understand why it stopped, and reclaim the tens of gigabytes that Docker will have quietly accumulated on your disk.

Installing Docker Engine on Linux

On Linux, Docker runs natively: no intermediate VM, no translation layer. It is the simplest and highest-performing environment. The recommended method uses Docker’s official repository, which guarantees updates and the presence of modern plugins.

On Debian and Ubuntu:

# 1. Remove old or competing packages, if present
for p in docker.io docker-doc docker-compose podman-docker containerd runc; do
  sudo apt-get remove -y "$p"
done

# 2. Add Docker's official GPG key
sudo apt-get update
sudo apt-get install -y 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

# 3. Declare the repository (the distribution codename is detected)
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
  | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# 4. Install the engine AND the plugins (buildx, compose)
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

The last two packages are the ones people forget, and they are the ones that provide docker buildx and docker compose. For Debian, replace both occurrences of ubuntu with debian in the URLs. For Fedora or RHEL, the equivalent procedure uses dnf and the docker-ce.repo repository; it is detailed at docs.docker.com/engine/install.

You still need to make sure the daemon starts, both now and at every reboot of the machine:

# --now starts the service, enable activates it at boot
sudo systemctl enable --now docker

# Check its status
systemctl status docker --no-pager

There is an alternative for a throwaway workstation or a test machine: the official installation script.

curl -fsSL https://get.docker.com -o get-docker.sh
# Always read a script before running it as root
less get-docker.sh
sudo sh get-docker.sh

It is convenient, but Docker explicitly advises against it in production: it guesses your distribution and makes decisions on your behalf, which is not what you want on a server that must be rebuildable identically.

”permission denied”: the docker group

Here is the error that awaits everyone on their first command:

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

The daemon listens on a UNIX socket, /var/run/docker.sock, owned by root and the docker group. Your user belongs to that group only if you have added it.

# The group already exists after installation; the command is idempotent
sudo groupadd -f docker

# Add your user (-a = append, essential:
# without it, usermod REPLACES the list of groups)
sudo usermod -aG docker "$USER"

# Group membership is read when a session opens.
# Open a new session, or force it for the current shell:
newgrp docker

# Verify
id -nG | tr ' ' '\n' | grep docker
docker run --rm hello-world

Forgetting the -a in usermod -aG is a costly mistake: it removes the user from all their other groups, sudo included.

You need to know what this change actually allows. The daemon runs as root, and the API it exposes lets you, among other things, mount any host directory into a container. Belonging to the docker group is therefore, in practice, equivalent to having superuser access to the machine. That is acceptable on your workstation; on a shared server, it deserves a conscious decision. For cases where this privilege escalation is unacceptable, Docker offers a rootless mode that runs the daemon under your own account.

macOS and Windows: the detour through a virtual machine

A reminder from lesson 1: a Linux image requires a Linux kernel. Neither macOS nor Windows provides one. On these systems, Docker Desktop installs and manages a small Linux VM for you; the docker client in your terminal talks to the daemon running inside it. Everything works transparently, but two consequences regularly make themselves felt: I/O on files shared with the host is slower, and the memory allocated to the VM is a global ceiling.

On macOS, installation uses the .dmg package downloaded from docs.docker.com/desktop/install/mac-install, or Homebrew:

brew install --cask docker
# Then launch the Docker application once: it is what
# installs the daemon and starts the VM.
docker version

Watch out for the architecture: on Apple Silicon Macs (M1 and later), the processor is arm64. An image published only for amd64 will be emulated, slowly, or will refuse to start. You can force the architecture when needed:

# See the daemon's architecture
docker info --format '{{.Architecture}}'

# Force a platform, at the cost of emulation
docker run --rm --platform linux/amd64 alpine:3.20 uname -m

On Windows, the right configuration is WSL2 (Windows Subsystem for Linux version 2), which provides a real Linux kernel. Docker Desktop’s old “Hyper-V” mode should be avoided, and “Windows containers” mode addresses a different need (Windows Server images).

# In PowerShell as administrator: installs WSL2 and a default Ubuntu
wsl --install

# Make sure version 2 is the one used by default
wsl --set-default-version 2

# Verify: the VERSION column must show 2
wsl -l -v

Next, install Docker Desktop, then enable the distribution you use under Settings > Resources > WSL Integration in its settings. You will then be able to type your docker commands directly from the Ubuntu shell.

One performance tip that changes everything on Windows: work inside the Linux file system, that is, under /home/you/projects, not under /mnt/c/Users/.... Files mounted from the Windows disk go through a translation layer; an npm install can be ten times slower there.

Verifying that the installation is healthy

Three commands are enough, and each answers a different question.

# 1. Do the client and the daemon talk to each other, and in which version?
#    If the "Server" section is missing, the daemon is unreachable.
docker version

# 2. Detailed state: storage driver, number of containers, memory
docker info

# 3. Does the full path work: pull from a registry + execution
docker run --rm hello-world

The third is the real end-to-end test: it validates DNS resolution, registry access, writing to the local cache, and container creation. The --rm flag deletes the container as soon as it exits — a habit to adopt for any throwaway container, otherwise they pile up.

Anatomy of docker run

docker run is the most commonly used and densest command. It actually chains three operations: download the image if it is absent, create a container, start it.

docker run [OPTIONS] IMAGE [COMMAND] [ARGUMENTS...]

The order is not negotiable: what comes before the image name configures Docker, and what follows it is passed to the process in the container. Placing -p 8080:80 after the image name does not produce a syntax error — it passes -p 8080:80 as an argument to the launched program, which is then surprised by it.

The options that matter, with what they really do:

docker run \
  -d \                       # detached: returns the terminal, the container runs in the background
  --name api \               # stable name, otherwise Docker invents a random one
  -p 8080:80 \               # publishes the port: 8080 on the host -> 80 in the container
  -e LOG_LEVEL=debug \       # environment variable
  -v mes-donnees:/data \     # named volume mounted on /data
  -w /app \                  # initial working directory
  --restart unless-stopped \ # restarts after a crash or a host reboot
  --memory 512m \            # memory limit (cgroups)
  nginx:1.27-alpine

Two modes stand in contrast and serve distinct needs. -d detaches: this is the mode for services. -it combines -i (keep standard input open) and -t (allocate a pseudo-terminal): this is the mode for interactive sessions.

# A throwaway shell session in an Alpine, deleted on exit
docker run --rm -it alpine:3.20 sh

# A temporary Python shell, without installing Python on the host
docker run --rm -it python:3.12-slim python

# Replace the image's default command
docker run --rm nginx:1.27-alpine nginx -v

That last line illustrates an idea worth remembering: the command declared in the image is only a default value, which can be overridden at launch.

The lifecycle of a container

A container goes through states, and a lot of confusion comes from reading docker ps too quickly.

                 docker create
    (image) ─────────────────────> created
                                     │ docker start
                                     v
      docker stop / process exits
  exited <───────────────────────  running  <──> paused
    │                                │        (pause/unpause)
    │ docker start                   │ docker restart
    │                                v
    └──> docker rm ──> (gone)      running
# Shows ONLY running containers
docker ps

# -a also shows stopped containers. This is the go-to command
# when "my container disappeared": it's there, with an Exited status.
docker ps -a

# Custom format, much more readable than the default output
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}'

The counterintuitive point: a container lives only as long as its main process (PID 1). It does not “contain” a powered-on machine sitting there waiting. If its process returns, the container goes to Exited, immediately.

# This container stops instantly: echo finishes its work.
docker run --name test-court alpine:3.20 echo "bonjour"
docker ps -a --filter name=test-court   # Exited (0)

# This one runs for 30 seconds, then exits.
docker run -d --name test-sleep alpine:3.20 sleep 30

This explains the classic puzzle “my container stops on its own”: the image does not launch any long-running service, or the service puts itself in the background and returns — which you should never do in a container.

The control commands:

docker stop api      # SIGTERM, then SIGKILL after a 10 s grace period
docker stop -t 30 api # extend the grace period to 30 s
docker kill api      # immediate SIGKILL, no chance to finish cleanly
docker start api     # restarts an existing container (disk state preserved)
docker restart api
docker rm api        # removes the stopped container, and its writable layer
docker rm -f api     # stops and removes in one go

The difference between stop and kill is not cosmetic. stop sends SIGTERM, giving the application time to close its connections and flush its buffers; kill cuts it off abruptly, with the risk of corrupting data being written. A properly containerized application must handle SIGTERM.

Getting into a container and observing it

When a service is not responding, the instinct is to look inside. Three tools, in this order.

First, the logs, because they are enough most of the time. Docker captures the standard output and standard error of the main process.

docker logs api                    # everything since startup
docker logs -f api                 # follow live (like tail -f)
docker logs --tail 50 api          # the last 50 lines
docker logs --since 10m api        # the last 10 minutes
docker logs --timestamps api       # with timestamps

A crucial corollary: a containerized application must write its logs to standard output, not to an internal file. Otherwise docker logs is empty and its logs die with the container.

Next, open a shell in a running container:

# bash when the image has it (Debian, Ubuntu)
docker exec -it api bash

# sh on Alpine images and minimal images: bash isn't there
docker exec -it api sh

# Run a single command without an interactive session
docker exec api ls -la /etc/nginx
docker exec api env

Note the distinction carefully: docker exec adds a process to a container that is already running, whereas docker run creates a new one. And everything you modify during this session lives in the container’s writable layer: it will be lost when the container is removed. This mode is for diagnosing, never for configuring durably.

Finally, inspection and measurements:

# Toute la configuration du conteneur, en JSON
docker inspect api

# Cibler une valeur precise avec un modele Go
docker inspect --format '{{.State.Status}}' api
docker inspect --format '{{.State.ExitCode}}' api
docker inspect --format '{{range $p, $c := .NetworkSettings.Ports}}{{$p}} {{end}}' api

# Consommation en direct (CPU, memoire, reseau, E/S)
docker stats

# Processus tournant dans le conteneur
docker top api

# Fichiers modifies par rapport a l'image (A ajoute, C change, D supprime)
docker diff api

# Copier un fichier dans les deux sens
docker cp api:/etc/nginx/nginx.conf ./nginx.conf
docker cp ./nginx.conf api:/etc/nginx/nginx.conf

The exit code deserves attention: 137 almost always means the process received a SIGKILL (128 + 9), often because it exceeded its memory limit and the kernel killed it. No application error message will give you that information.

Cleaning Up: Docker Silently Fills Your Disk

Every image you download, every stopped container, and every intermediate build layer stays on disk. On an active development machine, that quickly adds up to tens of gigabytes.

# Le bilan : ce que Docker occupe, et ce qui est recuperable
docker system df

# Detail par objet
docker system df -v

Cleanup goes in order of increasing aggressiveness.

# 1. Les conteneurs arretes
docker container prune

# 2. Les images sans nom (les "dangling", <none>), produites par les rebuilds
docker image prune

# 3. Les volumes qu'aucun conteneur n'utilise
#    ATTENTION : ce sont vos donnees. Verifiez avant.
docker volume ls
docker volume prune

# 4. Le cache de build, souvent le plus gros poste
docker builder prune

# 5. Tout d'un coup, images non utilisees comprises
docker system prune -a

docker system prune -a is the command to know and to use with caution: it deletes every image that no container is using, including ones you’ll have to pull again tomorrow. Without -a, it is limited to truly orphaned objects.

A few handy command chains, to understand before you paste them:

# Arreter tous les conteneurs en cours (-q ne renvoie que les identifiants)
docker stop $(docker ps -q)

# Supprimer tous les conteneurs, y compris ceux qui tournent
docker rm -f $(docker ps -aq)

# Supprimer les images d'un depot donne
docker images --filter reference='mon-app' -q | xargs -r docker rmi

A note for macOS and Windows: the space used by the Docker Desktop VM isn’t always returned to the host system after a prune. The VM’s disk file stays sized to its peak. Docker Desktop offers a space reclamation action in its settings.

Key Takeaways

  • On Linux, install from the official Docker repository, never from the distribution’s package, and don’t forget docker-buildx-plugin and docker-compose-plugin: without them, docker compose doesn’t exist.
  • The permission denied error on /var/run/docker.sock is fixed with sudo usermod -aG docker $USER followed by a new session. Be aware that this is equivalent to root access on the machine.
  • On macOS and Windows, Docker relies on a Linux VM. On Windows, use WSL2 and keep your projects in the Linux file system, not under /mnt/c, or you’ll face significant slowdowns.
  • A container lives exactly as long as its PID 1 process. “It stops on its own” almost always means that process has returned. docker ps -a shows stopped containers; docker ps does not.
  • Day-to-day work comes down to eight commands: run, ps -a, logs -f, exec -it, stop, rm, inspect, system df. Make --rm a reflex for any throwaway container.
  • Check docker system df regularly: images, volumes, and build caches fill a disk without warning.

Tip: run docker run --rm -it alpine:3.20 sh and explore freely. A throwaway container is the safest experimentation environment there is: exit, and nothing is left behind.

Comments

Comments are powered by GitHub Discussions

Sign in with GitHub to join the discussion

Link copied!