Docker is a platform for building and running containers. Containers are software units that package source code and its dependencies inside isolated environments. They’re usually ephemeral, with the contents of a container’s filesystem disposed of when it stops, but you can also store data persistently using features such as volumes.
Containers can be started anywhere an OCI container runtime is available, whether on your workstation with Docker, or in the cloud using orchestrators like Kubernetes. This narrows the gap between development and production.
In this guide, we’ll walk through installing and getting started with Docker. You’ll learn the basic concepts and see practical examples of the most common container management tasks.
Nowadays Docker comes in two main flavors: Docker Engine and Docker Desktop. Engine is a daemon and CLI for Linux systems, whereas Desktop uses virtualization to provide a consistent Docker experience on Windows, Mac, and Linux. It includes a graphical interface for managing your containers, in addition to the docker CLI from Engine. Although this article focuses on the traditional CLI experience, all the actions you’ll see in the following sections can also be achieved using Desktop’s GUI.
If you’re on Windows or Mac, Desktop is the easiest way to install Docker. Head to the documentation’s installation page and download the correct package for your system. Run the installer and then start Docker Desktop from your applications menu. Now you’re ready to run docker commands in your terminal.
To get Docker Engine on Linux, you can either download a prebuilt static binary, or follow the steps in the documentation to install from your distribution’s package manager. You’ll need to add Docker’s repository and GPG signing key first.
Installing Docker Engine on Ubuntu
The following steps work for Ubuntu and similar distributions. First, make sure you’ve got all the prerequisite packages for the installation process:
$ sudo apt-get update $ sudo apt-get install -y ca-certificates curl gnupg lsb-release
Next, download and save the GPG key used to sign the Docker repositories:
$ sudo mkdir -p /etc/apt/keyrings $ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
This allows the apt package manager to verify the authenticity of Docker’s packages. Now add the repository to your package lists:
$ echo \ "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
The command automatically selects the correct repository for your operating system version and processor architecture.
Update your package lists to include the new repository:
$ sudo apt-get update
Finally, install the
docker-ce-cli, and containerd packages:
$ sudo apt-get install -y docker-ce docker-ce-cli containerd.io
They have the following roles:
docker-ce– The Docker daemon that manages your containers.
docker-ce-cli– The CLI you use to interact with the daemon.
containerd.io– Containerd is a container runtime; it wraps operating system features such as cgroups to start and run your containers, on request from the Docker daemon.
Complete the installation by adding yourself to the docker group. This allows you to run docker CLI commands without using sudo. Logout and back in again to apply this change.
$ sudo groupadd docker $ sudo usermod -aG docker $USER
Checking Docker Is Working
Once you’ve installed Docker, you can check it’s working by starting a basic container. The hello-world image on Docker Hub is a good choice.
Run the following command to start a container with the image:
$ docker run hello-world:latest
You should see output similar to the following:
Unable to find image 'hello-world:latest' locally latest: Pulling from library/hello-world 2db29710123e: Pull complete Digest: sha256:c77be1d3a47d0caf71a82dd893ee61ce01f32fc758031a6ec4cf1389248bb833 Status: Downloaded newer image for hello-world:latest Hello from Docker! This message shows that your installation appears to be working correctly. To generate this message, Docker took the following steps: 1. The Docker client contacted the Docker daemon. 2. The Docker daemon pulled the "hello-world" image from the Docker Hub. (amd64) 3. The Docker daemon created a new container from that image which runs the executable that produces the output you are currently reading. 4. The Docker daemon streamed that output to the Docker client, which sent it to your terminal.
The first few lines show the Docker daemon recognizing that you’ve never used the
hello-world image before, so it has to download it from Docker Hub. It then starts the container and streams its output to your terminal. All the lines from “Hello from Docker” onwards are emitted by the process inside the container.
Your Docker installation is ready to use.
Before you start properly using Docker, it’s important to understand the two main concepts you’ll encounter:
- Images – All containers are started from an image. The image defines the initial state of the container’s filesystem. It’s similar to the install media you’d use with traditional virtual machines. Prebuilt images are available in public repositories such as Docker Hub, including options for popular operating systems and applications.
- Containers – A container is an instance of an image. It’s a running process independent of your other containers and the existing processes on your host. You can start multiple containers that use the same image simultaneously. Containers have a writable filesystem layer applied on top of the image, allowing modifications to be made.
Now you can begin confidently working with containers (running processes) and images, the reusable templates you start containers from.
Packaging software as a container makes it more portable, allowing you to eliminate discrepancies between environments. You can use the container on your laptop, in production, and within your CI/CD infrastructure. Take a look at how Spacelift uses Docker containers to run CI jobs inside isolated environments.
Ready to use Docker? Let’s jump in!
Running a Container
docker run command is used to start containers from an image. You ran a simple container in the section above when you checked your installation:
$ docker run hello-world:latest
The command accepts the name of the image to run as its argument. The last part of the image name, after the colon, is the tag, which is used to identify different versions and variants.
latest tag is a convention for the newest release of the image, but beware, this is not standardized and can expose you to breaking changes. It’s best practice to use a more precise tag when available, such as
Detaching From Your Container
hello-world image runs a process and then exits immediately. Most real-world containerized applications aren’t like this, however. They’re normally long-lived processes such as servers that run indefinitely, until the container is stopped.
-d flag instructs the Docker CLI to detach from the container, leaving the process running in the background. You’ll be dropped back to your terminal prompt.
$ docker run -d nginx:latest
Creating an Image
Images are created from Dockerfiles. These are text files containing a list of instructions that assemble the image’s filesystem. Copy the following sample and save it to a file called
Dockerfile in a new directory:
FROM node:latest COPY app.js . ENTRYPOINT ["node"] CMD ["app.js"]
app.js in the same directory:
Now run the following command to build your image:
$ docker build -t demo-image:latest . ... Successfully built 80890ed154fa Successfully tagged demo-image:latest
The command instructs Docker to build the Dockerfile in your working directory (indicated by the
. argument) and tag the output as
demo-image:latest. The following steps occur:
FROMstatement instructs Docker to use the
node:latestimage as the base for your new image. The instructions that come afterwards apply on top of the
COPYstatement copies your application’s source code from your working directory, into the container.
- The image is configured so node is run when the container starts, with your app’s code as its argument.
Try starting a container using your image:
$ docker run demo-image:latest It works!
Your code has been executed inside a containerized Node.js environment!
Pushing Images to a Registry
Pushing to a registry is the next step in image authorship. At the moment, your image is confined to your own machine. Registries allow images to be distributed among peers, team members, and the broader community.
Docker Hub is the biggest registry, and you can sign up for free. Before you can push to your account, you’ll need to authenticate the Docker CLI by running
docker login and following the prompts.
$ docker login
Next you must specially tag your image so it references your Docker Hub username. The following command adds another tag to your existing image – replace
<DOCKER_HUB_USERNAME> with your real Docker Hub username:
$ docker tag demo-image:latest <DOCKER_HUB_USERNAME>/demo-image:latest
Use the docker push command to upload your image. The Docker CLI automatically infers the registry URL from the image’s tag.
$ docker push <DOCKER_HUB_USERNAME>/demo-image:latest
Now other users can start containers from your image, even when it’s not already on their machine!
$ docker run <DOCKER_HUB_USERNAME>/demo-image:latest
Listing Containers on Your Host
You can list the containers on your host with the
docker ps command. This defaults to only showing currently running containers:
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS 0000ac4e7eba nginx:latest "/docker-entrypoint...." 3 seconds ago Up 1 minute
To see all your containers, including stopped and exited ones, add the
$ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS 0000ac4e7eba nginx:latest "/docker-entrypoint...." 3 seconds ago Up 1 minute 8cf6ed7f95fe hello-world:latest "/hello" 2 minutes ago Exited (0) 2 minutes ago a45b50521fc0 hello-world:latest "/hello" 2 minutes ago Exited (0) 2 minutes ago
The command provides each container’s ID and current status. To get more detailed information about an individual container, pass its ID or name to
$ docker inspect 8cf6ed7f95fe
This produces verbose JSON output that includes all the data associated with the container.
Starting, Stopping, and Deleting Containers
Detached containers with long-living processes can be stopped by passing their ID or name to the
docker stop command:
$ docker run --name nginx -d nginx:latest 87a116e59f1815fda7dd666f136e5f9f3d085bbd80889c0e0e5836e95e9511ed $ docker stop nginx nginx
docker start command to restart the container with the same configuration. Note that any filesystem modifications made while it was previously running will be lost.
$ docker start nginx
To remove a container, run
docker rm with the container’s ID or name:
$ docker rm nginx
If the container’s running, you’ll need to add the
--force flag to confirm your intentions:
$ docker rm nginx --force
Listing and Removing Images
docker images command lists the images on your host, including their age and size:
$ docker images REPOSITORY TAG IMAGE ID CREATED SIZE nginx latest 3964ce7b8458 6 days ago 142MB hello-world latest feb5d9fea6a5 15 months ago 13.3kB
To delete an image, pass its ID or tag to
$ docker rmi hello-world:latest
Images can’t be deleted if they’re being used by a running container. Remove the container first, then the image.
Now you’ve learned the basics, here are some more advanced techniques for real-world applications.
Port Forwarding Into Containers
Port forwarding allows you to access applications running inside a container using your host’s network interfaces. In some of the examples above, a container running the NGINX web server was started using the
nginx:latest image. As no port was bound, NGINX couldn’t be accessed from outside your Docker host.
To bind a port, add the
-p flag when you start a container:
$ docker run --name nginx -p 8800:80 -d nginx:latest
This example binds port 8800 on your host to port 80 inside the container, which NGINX listens on.
Now you can send a request to
localhost:8800 and get a response from your containerized web server:
$ curl localhost:8800 <!DOCTYPE html> <html> <head> <title>Welcome to nginx!</title> ...
Using Volumes to Store Container Data
Containers are meant to be ephemeral. Although their filesystems are writable, any changes you make are lost after the container stops. You shouldn’t assume a container will run indefinitely, as it could be stopped due to a crash in your application or a host reboot.
Volumes are a mechanism for persisting data inside containers. Volumes are storage units that you mount into the container’s filesystem. Data written to the volume is stored outside the container, so it can be reattached to a replacement container after the original stops.
You can mount a volume using the
-v flag for
$ docker run -v my_volume:/container/mount/point my-image:latest
Alternatively, you can bind mount a directory in your host’s filesystem directly into the container:
$ docker run -v $PWD:/container/mount/point my-image:latest
This approach is ideal for development use. Changes you make to a bound source code directory will be immediately reflected in the container.
Viewing Container Logs
Docker automatically collects logs from the standard output and error streams of your container’s foreground process. You can access these logs by passing the container’s ID or name to the
docker logs command:
$ docker logs nginx 172.17.0.1 - - [20/Dec/2022:21:01:52 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.81.0" "-" 172.17.0.1 - - [20/Dec/2022:21:10:02 +0000] "GET /about HTTP/1.1" 404 153 "-" "curl/7.81.0" "-" 172.17.0.1 - - [20/Dec/2022:21:10:04 +0000] "GET /demo HTTP/1.1" 404 153 "-" "curl/7.81.0" "-" 172.17.0.1 - - [20/Dec/2022:21:10:06 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.81.0" "-"
This allows you to review your application’s activity and spot any errors.
To continually stream new logs into your terminal, add the
--follow flag to the command:
$ docker logs nginx --follow
Hit Ctrl+C to stop watching the logs.
Getting a Shell to a Container
One final tip: sometimes you might need to get inside a container, to debug problems or run admin tools included with an image. You can run a process in an existing container using the
docker exec command:
$ docker exec <CONTAINER> <COMMAND>
Pass the container ID or name as
<CONTAINER>, then the command and its arguments as
<COMMAND>. If you use the name of a shell as the command, you’ll get an interactive terminal session, within the container. For this to work, you must also pass the
-t flags to
docker exec, to attach your terminal’s standard input stream and set up a TTY.
$ docker exec -it nginx bash root@58661670f1ad:/#
Now you can inspect the container’s filesystem and run any commands you need.
Docker is the most popular containerization platform for developers. You can run containers and build images with the docker CLI, or opt for Docker Desktop if you prefer a graphical interface. Images created by Docker can be used in any other OCI-compatible container environment too, such as Podman or Kubernetes.
The most Flexible CI/CD Automation Tool
Spacelift is an alternative to using homegrown solutions on top of a generic CI. It helps overcome common state management issues and adds several must-have capabilities for infrastructure management.