Get Started

What is Docker?

Docker is an open platform for developing, shipping, and running applications. Docker enables you to separate your applications from your infrastructure so you can deliver software quickly. With Docker, you can manage your infrastructure in the same ways you manage your applications. By taking advantage of Docker's methodologies for shipping, testing, and deploying code, you can significantly reduce the delay between writing code and running it in production.

The Docker platform

Docker provides the ability to package and run an application in a loosely isolated environment called a container. The isolation and security let you run many containers simultaneously on a given host. Containers are lightweight and contain everything needed to run the application, so you don't need to rely on what's installed on the host. You can share containers while you work, and be sure that everyone you share with gets the same container that works in the same way.

Docker provides tooling and a platform to manage the lifecycle of your containers:

What can I use Docker for?

Fast, consistent delivery of your applications

Docker streamlines the development lifecycle by allowing developers to work in standardized environments using local containers which provide your applications and services. Containers are great for continuous integration and continuous delivery (CI/CD) workflows.

Consider the following example scenario:

Responsive deployment and scaling

Docker's container-based platform allows for highly portable workloads. Docker containers can run on a developer's local laptop, on physical or virtual machines in a data center, on cloud providers, or in a mixture of environments.

Docker's portability and lightweight nature also make it easy to dynamically manage workloads, scaling up or tearing down applications and services as business needs dictate, in near real time.

Running more workloads on the same hardware

Docker is lightweight and fast. It provides a viable, cost-effective alternative to hypervisor-based virtual machines, so you can use more of your server capacity to achieve your business goals. Docker is perfect for high density environments and for small and medium deployments where you need to do more with fewer resources.

Docker architecture

Docker uses a client-server architecture. The Docker client talks to the Docker daemon, which does the heavy lifting of building, running, and distributing your Docker containers. The Docker client and daemon can run on the same system, or you can connect a Docker client to a remote Docker daemon. The Docker client and daemon communicate using a REST API, over UNIX sockets or a network interface. Another Docker client is Docker Compose, that lets you work with applications consisting of a set of containers.

Docker Architecture diagram

The Docker daemon

The Docker daemon (dockerd) listens for Docker API requests and manages Docker objects such as images, containers, networks, and volumes. A daemon can also communicate with other daemons to manage Docker services.

The Docker client

The Docker client (docker) is the primary way that many Docker users interact with Docker. When you use commands such as docker run, the client sends these commands to dockerd, which carries them out. The docker command uses the Docker API. The Docker client can communicate with more than one daemon.

Docker Desktop

Docker Desktop is an easy-to-install application for your Mac, Windows, or Linux environment that enables you to build and share containerized applications and microservices. Docker Desktop includes the Docker daemon (dockerd), the Docker client (docker), Docker Compose, Docker Content Trust, Kubernetes, and Credential Helper. For more information, see Docker Desktop.

Docker registries

A Docker registry stores Docker images. Docker Hub is a public registry that anyone can use, and Docker looks for images on Docker Hub by default. You can even run your own private registry.

When you use the docker pull or docker run commands, Docker pulls the required images from your configured registry. When you use the docker push command, Docker pushes your image to your configured registry.

Docker objects

When you use Docker, you are creating and using images, containers, networks, volumes, plugins, and other objects. This section is a brief overview of some of those objects.

Images

An image is a read-only template with instructions for creating a Docker container. Often, an image is based on another image, with some additional customization. For example, you may build an image that is based on the Ubuntu image but includes the Apache web server and your application, as well as the configuration details needed to make your application run.

You might create your own images or you might only use those created by others and published in a registry. To build your own image, you create a Dockerfile with a simple syntax for defining the steps needed to create the image and run it. Each instruction in a Dockerfile creates a layer in the image. When you change the Dockerfile and rebuild the image, only those layers which have changed are rebuilt. This is part of what makes images so lightweight, small, and fast, when compared to other virtualization technologies.

Containers

A container is a runnable instance of an image. You can create, start, stop, move, or delete a container using the Docker API or CLI. You can connect a container to one or more networks, attach storage to it, or even create a new image based on its current state.

By default, a container is relatively well isolated from other containers and its host machine. You can control how isolated a container's network, storage, or other underlying subsystems are from other containers or from the host machine.

A container is defined by its image as well as any configuration options you provide to it when you create or start it. When a container is removed, any changes to its state that aren't stored in persistent storage disappear.

Example docker run command

The following command runs an ubuntu container, attaches interactively to your local command-line session, and runs /bin/bash.

$ docker run -i -t ubuntu /bin/bash

When you run this command, the following happens (assuming you are using the default registry configuration):

  1. If you don't have the ubuntu image locally, Docker pulls it from your configured registry, as though you had run docker pull ubuntu manually.

  2. Docker creates a new container, as though you had run a docker container create command manually.

  3. Docker allocates a read-write filesystem to the container, as its final layer. This allows a running container to create or modify files and directories in its local filesystem.

  4. Docker creates a network interface to connect the container to the default network, since you didn't specify any networking options. This includes assigning an IP address to the container. By default, containers can connect to external networks using the host machine's network connection.

  5. Docker starts the container and executes /bin/bash. Because the container is running interactively and attached to your terminal (due to the -i and -t flags), you can provide input using your keyboard while Docker logs the output to your terminal.

  6. When you run exit to terminate the /bin/bash command, the container stops but isn't removed. You can start it again or remove it.

The underlying technology

Docker is written in the Go programming language and takes advantage of several features of the Linux kernel to deliver its functionality. Docker uses a technology called namespaces to provide the isolated workspace called the container. When you run a container, Docker creates a set of namespaces for that container.

These namespaces provide a layer of isolation. Each aspect of a container runs in a separate namespace and its access is limited to that namespace.

Next steps

Get Docker

Docker is an open platform for developing, shipping, and running applications.

Docker allows you to separate your applications from your infrastructure so you can deliver software quickly. With Docker, you can manage your infrastructure in the same ways you manage your applications.

By taking advantage of Docker’s methodologies for shipping, testing, and deploying code quickly, you can significantly reduce the delay between writing code and running it in production.

You can download and install Docker on multiple platforms. Refer to the following section and choose the best installation path for you.

Docker Desktop terms

Commercial use of Docker Desktop in larger enterprises (more than 250 employees OR more than $10 million USD in annual revenue) requires a paid subscription.

{{< card title="Docker Desktop for Mac" description="A native application using the macOS sandbox security model that delivers all Docker tools to your Mac." link="/desktop/setup/install/mac-install/" icon="/icons/AppleMac.svg" >}}

{{< card title="Docker Desktop for Windows" description="A native Windows application that delivers all Docker tools to your Windows computer." link="/desktop/setup/install/windows-install/" icon="/icons/Windows.svg" >}}

{{< card title="Docker Desktop for Linux" description="A native Linux application that delivers all Docker tools to your Linux computer." link="/desktop/setup/install/linux/" icon="/icons/Linux.svg" >}}

[!NOTE]

If you're looking for information on how to install Docker Engine, see Docker Engine installation overview.

Educational resources

Docker and the broader community of Docker experts have put together many different ways to get further training and hands-on experience with Docker. Expand your understanding of Docker and Kubernetes with these additional free and paid resources.

Docker Training

Start with the containerization tutorial for a guided introduction to running, building, and sharing containerized applications.

Books

If books are your preferred learning style, check out these written by the Docker Captains. Docker Captain is a distinction that Docker awards to select members of the community that are both experts in their field and are committed to sharing their Docker knowledge with others.

CLI cheat sheet

The Docker CLI cheat sheet features the common Docker CLI commands for easy reference. It covers working with Images, Containers, Docker Hub, and other general purpose commands.

Self-Paced online learning

A number of Docker Captains have also created video courses on Docker and Kubernetes.

* Many of the courses are fee-based

Build, tag, and publish an image

{{< youtube-embed chiiGLlYRlY >}}

Explanation

In this guide, you will learn the following:

Building images

Most often, images are built using a Dockerfile. The most basic docker build command might look like the following:

docker build .

The final . in the command provides the path or URL to the build context. At this location, the builder will find the Dockerfile and other referenced files.

When you run a build, the builder pulls the base image, if needed, and then runs the instructions specified in the Dockerfile.

With the previous command, the image will have no name, but the output will provide the ID of the image. As an example, the previous command might produce the following output:

$ docker build .
[+] Building 3.5s (11/11) FINISHED                                              docker:desktop-linux
 => [internal] load build definition from Dockerfile                                            0.0s
 => => transferring dockerfile: 308B                                                            0.0s
 => [internal] load metadata for docker.io/library/python:3.12                                  0.0s
 => [internal] load .dockerignore                                                               0.0s
 => => transferring context: 2B                                                                 0.0s
 => [1/6] FROM docker.io/library/python:3.12                                                    0.0s
 => [internal] load build context                                                               0.0s
 => => transferring context: 123B                                                               0.0s
 => [2/6] WORKDIR /usr/local/app                                                                0.0s
 => [3/6] RUN useradd app                                                                       0.1s
 => [4/6] COPY ./requirements.txt ./requirements.txt                                            0.0s
 => [5/6] RUN pip install --no-cache-dir --upgrade -r requirements.txt                          3.2s
 => [6/6] COPY ./app ./app                                                                      0.0s
 => exporting to image                                                                          0.1s
 => => exporting layers                                                                         0.1s
 => => writing image sha256:9924dfd9350407b3df01d1a0e1033b1e543523ce7d5d5e2c83a724480ebe8f00    0.0s

With the previous output, you could start a container by using the referenced image:

docker run sha256:9924dfd9350407b3df01d1a0e1033b1e543523ce7d5d5e2c83a724480ebe8f00

That name certainly isn't memorable, which is where tagging becomes useful.

Tagging images

Tagging images is the method to provide an image with a memorable name. However, there is a structure to the name of an image. A full image name has the following structure:

[HOST[:PORT_NUMBER]/]PATH[:TAG]

Some examples of image names include:

To tag an image during a build, add the -t or --tag flag:

docker build -t my-username/my-image .

If you've already built an image, you can add another tag to the image by using the docker image tag command:

docker image tag my-username/my-image another-username/another-image:v1

Publishing images

Once you have an image built and tagged, you're ready to push it to a registry. To do so, use the docker push command:

docker push my-username/my-image

Within a few seconds, all of the layers for your image will be pushed to the registry.

Requiring authentication

Before you're able to push an image to a repository, you will need to be authenticated. To do so, simply use the docker login command. { .information }

Try it out

In this hands-on guide, you will build a simple image using a provided Dockerfile and push it to Docker Hub.

Set up

  1. Get the sample application.

    If you have Git, you can clone the repository for the sample application. Otherwise, you can download the sample application. Choose one of the following options.

    {{< tabs >}} {{< tab name="Clone with git" >}}

    Use the following command in a terminal to clone the sample application repository.

    $ git clone https://github.com/docker/getting-started-todo-app
    

    {{< /tab >}} {{< tab name="Download" >}}

    Download the source and extract it.

    {{< button url="https://github.com/docker/getting-started-todo-app/raw/cd61f824da7a614a8298db503eed6630eeee33a3/app.zip" text="Download the source" >}}

    {{< /tab >}} {{< /tabs >}}

  2. Download and install Docker Desktop.

  3. If you don't have a Docker account yet, create one now. Once you've done that, sign in to Docker Desktop using that account.

Build an image

Now that you have a repository on Docker Hub, it's time for you to build an image and push it to the repository.

  1. Using a terminal in the root of the sample app repository, run the following command. Replace YOUR_DOCKER_USERNAME with your Docker Hub username:

    $ docker build -t <YOUR_DOCKER_USERNAME>/concepts-build-image-demo .
    

    As an example, if your username is mobywhale, you would run the command:

    $ docker build -t mobywhale/concepts-build-image-demo .
    
  2. Once the build has completed, you can view the image by using the following command:

    $ docker image ls
    

    The command will produce output similar to the following:

    REPOSITORY                             TAG       IMAGE ID       CREATED          SIZE
    mobywhale/concepts-build-image-demo    latest    746c7e06537f   24 seconds ago   354MB
    
  3. You can actually view the history (or how the image was created) by using the docker image history command:

    $ docker image history mobywhale/concepts-build-image-demo
    

    You'll then see output similar to the following:

    IMAGE          CREATED         CREATED BY                                      SIZE      COMMENT
    f279389d5f01   8 seconds ago   CMD ["node" "./src/index.js"]                   0B        buildkit.dockerfile.v0
    <missing>      8 seconds ago   EXPOSE map[3000/tcp:{}]                         0B        buildkit.dockerfile.v0 
    <missing>      8 seconds ago   WORKDIR /app                                    8.19kB    buildkit.dockerfile.v0
    <missing>      4 days ago      /bin/sh -c #(nop)  CMD ["node"]                 0B
    <missing>      4 days ago      /bin/sh -c #(nop)  ENTRYPOINT ["docker-entry…   0B
    <missing>      4 days ago      /bin/sh -c #(nop) COPY file:4d192565a7220e13…   20.5kB
    <missing>      4 days ago      /bin/sh -c apk add --no-cache --virtual .bui…   7.92MB
    <missing>      4 days ago      /bin/sh -c #(nop)  ENV YARN_VERSION=1.22.19     0B
    <missing>      4 days ago      /bin/sh -c addgroup -g 1000 node     && addu…   126MB
    <missing>      4 days ago      /bin/sh -c #(nop)  ENV NODE_VERSION=20.12.0     0B
    <missing>      2 months ago    /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
    <missing>      2 months ago    /bin/sh -c #(nop) ADD file:d0764a717d1e9d0af…   8.42MB
    

    This output shows the layers of the image, highlighting the layers you added and those that were inherited from your base image.

Push the image

Now that you have an image built, it's time to push the image to a registry.

  1. Push the image using the docker push command:

    $ docker push <YOUR_DOCKER_USERNAME>/concepts-build-image-demo
    

    If you receive a requested access to the resource is denied, make sure you are both logged in and that your Docker username is correct in the image tag.

    After a moment, your image should be pushed to Docker Hub.

Additional resources

To learn more about building, tagging, and publishing images, visit the following resources:

Next steps

Now that you have learned about building and publishing images, it's time to learn how to speed up the build process using the Docker build cache.

{{< button text="Using the build cache" url="using-the-build-cache" >}}

Multi-stage builds

{{< youtube-embed vR185cjwxZ8 >}}

Explanation

In a traditional build, all build instructions are executed in sequence, and in a single build container: downloading dependencies, compiling code, and packaging the application. All those layers end up in your final image. This approach works, but it leads to bulky images carrying unnecessary weight and increasing your security risks. This is where multi-stage builds come in.

Multi-stage builds introduce multiple stages in your Dockerfile, each with a specific purpose. Think of it like the ability to run different parts of a build in multiple different environments, concurrently. By separating the build environment from the final runtime environment, you can significantly reduce the image size and attack surface. This is especially beneficial for applications with large build dependencies.

Multi-stage builds are recommended for all types of applications.

Here's a simplified example of a multi-stage build structure using pseudo-code. Notice there are multiple FROM statements and a new AS <stage-name>. In addition, the COPY statement in the second stage is copying --from the previous stage.

# Stage 1: Build Environment
FROM builder-image AS build-stage 
# Install build tools (e.g., Maven, Gradle)
# Copy source code
# Build commands (e.g., compile, package)

# Stage 2: Runtime environment
FROM runtime-image AS final-stage  
#  Copy application artifacts from the build stage (e.g., JAR file)
COPY --from=build-stage /path/in/build/stage /path/to/place/in/final/stage
# Define runtime configuration (e.g., CMD, ENTRYPOINT) 

This Dockerfile uses two stages:

Try it out

In this hands-on guide, you'll unlock the power of multi-stage builds to create lean and efficient Docker images for a sample Java application. You'll use a simple “Hello World” Spring Boot-based application built with Maven as your example.

  1. Download and install Docker Desktop.

  2. Open this pre-initialized project to generate a ZIP file. Here’s how that looks:

    A screenshot of Spring Initializr tool selected with Java 21, Spring Web and Spring Boot 3.4.0

    Spring Initializr is a quickstart generator for Spring projects. It provides an extensible API to generate JVM-based projects with implementations for several common concepts — like basic language generation for Java, Kotlin, Groovy, and Maven.

    Select Generate to create and download the zip file for this project.

    For this demonstration, you’ve paired Maven build automation with Java, a Spring Web dependency, and Java 21 for your metadata.

  3. Navigate the project directory. Once you unzip the file, you'll see the following project directory structure:

    spring-boot-docker
    ├── HELP.md
    ├── mvnw
    ├── mvnw.cmd
    ├── pom.xml
    └── src
        ├── main
        │   ├── java
        │   │   └── com
        │   │       └── example
        │   │           └── spring_boot_docker
        │   │               └── SpringBootDockerApplication.java
        │   └── resources
        │       ├── application.properties
        │       ├── static
        │       └── templates
        └── test
            └── java
                └── com
                    └── example
                        └── spring_boot_docker
                            └── SpringBootDockerApplicationTests.java
    
    15 directories, 7 files
    

    The src/main/java directory contains your project's source code, the src/test/java directory
    contains the test source, and the pom.xml file is your project’s Project Object Model (POM).

    The pom.xml file is the core of a Maven project's configuration. It's a single configuration file that
    contains most of the information needed to build a customized project. The POM is huge and can seem
    daunting. Thankfully, you don't yet need to understand every intricacy to use it effectively.

  4. Create a RESTful web service that displays "Hello World!".

    Under the src/main/java/com/example/spring_boot_docker/ directory, you can modify your
    SpringBootDockerApplication.java file with the following content:

    package com.example.spring_boot_docker;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    
    @RestController
    @SpringBootApplication
    public class SpringBootDockerApplication {
    
        @RequestMapping("/")
            public String home() {
            return "Hello World";
        }
    
    	public static void main(String[] args) {
    		SpringApplication.run(SpringBootDockerApplication.class, args);
    	}
    
    }
    

    The SpringbootDockerApplication.java file starts by declaring your com.example.spring_boot_docker package and importing necessary Spring frameworks. This Java file creates a simple Spring Boot web application that responds with "Hello World" when a user visits its homepage.

Create the Dockerfile

Now that you have the project, you’re ready to create the Dockerfile.

  1. Create a file named Dockerfile in the same folder that contains all the other folders and files (like src, pom.xml, etc.).

  2. In the Dockerfile, define your base image by adding the following line:

    FROM eclipse-temurin:21.0.8_9-jdk-jammy
    
  3. Now, define the working directory by using the WORKDIR instruction. This will specify where future commands will run and the directory files will be copied inside the container image.

    WORKDIR /app
    
  4. Copy both the Maven wrapper script and your project's pom.xml file into the current working directory /app within the Docker container.

    COPY .mvn/ .mvn
    COPY mvnw pom.xml ./
    
  5. Execute a command within the container. It runs the ./mvnw dependency:go-offline command, which uses the Maven wrapper (./mvnw) to download all dependencies for your project without building the final JAR file (useful for faster builds).

    RUN ./mvnw dependency:go-offline
    
  6. Copy the src directory from your project on the host machine to the /app directory within the container.

    COPY src ./src
    
  7. Set the default command to be executed when the container starts. This command instructs the container to run the Maven wrapper (./mvnw) with the spring-boot:run goal, which will build and execute your Spring Boot application.

    CMD ["./mvnw", "spring-boot:run"]
    

    And with that, you should have the following Dockerfile:

    FROM eclipse-temurin:21.0.8_9-jdk-jammy
    WORKDIR /app
    COPY .mvn/ .mvn
    COPY mvnw pom.xml ./
    RUN ./mvnw dependency:go-offline
    COPY src ./src
    CMD ["./mvnw", "spring-boot:run"]
    

Build the container image

  1. Execute the following command to build the Docker image:

    $ docker build -t spring-helloworld .
    
  2. Check the size of the Docker image by using the docker images command:

    $ docker images
    

    Doing so will produce output like the following:

    REPOSITORY          TAG       IMAGE ID       CREATED          SIZE
    spring-helloworld   latest    ff708d5ee194   3 minutes ago    880MB
    

    This output shows that your image is 880MB in size. It contains the full JDK, Maven toolchain, and more. In production, you don’t need that in your final image.

Run the Spring Boot application

  1. Now that you have an image built, it's time to run the container.

    $ docker run -p 8080:8080 spring-helloworld
    

    You'll then see output similar to the following in the container log:

    [INFO] --- spring-boot:3.3.4:run (default-cli) @ spring-boot-docker ---
    [INFO] Attaching agents: []
    
         .   ____          _            __ _ _
        /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
       ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
        \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
         '  |____| .__|_| |_|_| |_\__, | / / / /
        =========|_|==============|___/=/_/_/_/
    
        :: Spring Boot ::                (v3.3.4)
    
    2024-09-29T23:54:07.157Z  INFO 159 --- [spring-boot-docker] [           main]
    c.e.s.SpringBootDockerApplication        : Starting SpringBootDockerApplication using Java
    21.0.2 with PID 159 (/app/target/classes started by root in /app)
     ….
    
  2. Access your “Hello World” page through your web browser at http://localhost:8080, or via this curl command:

    $ curl localhost:8080
    Hello World
    

Use multi-stage builds

  1. Consider the following Dockerfile:

    FROM eclipse-temurin:21.0.8_9-jdk-jammy AS builder
    WORKDIR /opt/app
    COPY .mvn/ .mvn
    COPY mvnw pom.xml ./
    RUN ./mvnw dependency:go-offline
    COPY ./src ./src
    RUN ./mvnw clean install
    
    FROM eclipse-temurin:21.0.8_9-jre-jammy AS final
    WORKDIR /opt/app
    EXPOSE 8080
    COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar
    ENTRYPOINT ["java", "-jar", "/opt/app/*.jar"]
    

    Notice that this Dockerfile has been split into two stages.

    • The first stage remains the same as the previous Dockerfile, providing a Java Development Kit (JDK) environment for building the application. This stage is given the name of builder.

    • The second stage is a new stage named final. It uses a slimmer eclipse-temurin:21.0.8_9-jre-jammy image, containing just the Java Runtime Environment (JRE) needed to run the application. This image provides a Java Runtime Environment (JRE) which is enough for running the compiled application (JAR file).

    For production use, it's highly recommended that you produce a custom JRE-like runtime using jlink. JRE images are available for all versions of Eclipse Temurin, but jlink allows you to create a minimal runtime containing only the necessary Java modules for your application. This can significantly reduce the size and improve the security of your final image. Refer to this page for more information.

    With multi-stage builds, a Docker build uses one base image for compilation, packaging, and unit tests and then a separate image for the application runtime. As a result, the final image is smaller in size since it doesn’t contain any development or debugging tools. By separating the build environment from the final runtime environment, you can significantly reduce the image size and increase the security of your final images.

  2. Now, rebuild your image and run your ready-to-use production build.

    $ docker build -t spring-helloworld-builder .
    

    This command builds a Docker image named spring-helloworld-builder using the final stage from your Dockerfile file located in the current directory.

    [!NOTE]

    In your multi-stage Dockerfile, the final stage (final) is the default target for building. This means that if you don't explicitly specify a target stage using the --target flag in the docker build command, Docker will automatically build the last stage by default. You could use docker build -t spring-helloworld-builder --target builder . to build only the builder stage with the JDK environment.

  3. Look at the image size difference by using the docker images command:

    $ docker images
    

    You'll get output similar to the following:

    spring-helloworld-builder latest    c5c76cb815c0   24 minutes ago      428MB
    spring-helloworld         latest    ff708d5ee194   About an hour ago   880MB
    

    Your final image is just 428 MB, compared to the original build size of 880 MB.

    By optimizing each stage and only including what's necessary, you were able to significantly reduce the overall image size while still achieving the same functionality. This not only improves performance but also makes your Docker images more lightweight, more secure, and easier to manage.

Additional resources

Understanding the image layers

{{< youtube-embed wJwqtAkmtQA >}}

Explanation

As you learned in What is an image?, container images are composed of layers. And each of these layers, once created, are immutable. But, what does that actually mean? And how are those layers used to create the filesystem a container can use?

Image layers

Each layer in an image contains a set of filesystem changes - additions, deletions, or modifications. Let’s look at a theoretical image:

  1. The first layer adds basic commands and a package manager, such as apt.
  2. The second layer installs a Python runtime and pip for dependency management.
  3. The third layer copies in an application’s specific requirements.txt file.
  4. The fourth layer installs that application’s specific dependencies.
  5. The fifth layer copies in the actual source code of the application.

This example might look like:

screenshot of the flowchart showing the concept of the image layers

This is beneficial because it allows layers to be reused between images. For example, imagine you wanted to create another Python application. Due to layering, you can leverage the same Python base. This will make builds faster and reduce the amount of storage and bandwidth required to distribute the images. The image layering might look similar to the following:

screenshot of the flowchart showing the benefits of the image layering

Layers let you extend images of others by reusing their base layers, allowing you to add only the data that your application needs.

Stacking the layers

Layering is made possible by content-addressable storage and union filesystems. While this will get technical, here’s how it works:

  1. After each layer is downloaded, it is extracted into its own directory on the host filesystem.
  2. When you run a container from an image, a union filesystem is created where layers are stacked on top of each other, creating a new and unified view.
  3. When the container starts, its root directory is set to the location of this unified directory, using chroot.

When the union filesystem is created, in addition to the image layers, a directory is created specifically for the running container. This allows the container to make filesystem changes while allowing the original image layers to remain untouched. This enables you to run multiple containers from the same underlying image.

Try it out

In this hands-on guide, you will create new image layers manually using the docker container commit command. Note that you’ll rarely create images this way, as you’ll normally use a Dockerfile. But, it makes it easier to understand how it’s all working.

Create a base image

In this first step, you will create your own base image that you will then use for the following steps.

  1. Download and install Docker Desktop.

  2. In a terminal, run the following command to start a new container:

    $ docker run --name=base-container -ti ubuntu
    

    Once the image has been downloaded and the container has started, you should see a new shell prompt. This is running inside your container. It will look similar to the following (the container ID will vary):

    root@d8c5ca119fcd:/#
    
  3. Inside the container, run the following command to install Node.js:

    $ apt update && apt install -y nodejs
    

    When this command runs, it downloads and installs Node inside the container. In the context of the union filesystem, these filesystem changes occur within the directory unique to this container.

  4. Validate if Node is installed by running the following command:

    $ node -e 'console.log("Hello world!")'
    

    You should then see a “Hello world!” appear in the console.

  5. Now that you have Node installed, you’re ready to save the changes you’ve made as a new image layer, from which you can start new containers or build new images. To do so, you will use the docker container commit command. Run the following command in a new terminal:

    $ docker container commit -m "Add node" base-container node-base
    
  6. View the layers of your image using the docker image history command:

    $ docker image history node-base
    

    You will see output similar to the following:

    IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
    9e274734bb25   10 seconds ago   /bin/bash                                       157MB     Add node
    cd1dba651b30   7 days ago       /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
    <missing>      7 days ago       /bin/sh -c #(nop) ADD file:6089c6bede9eca8ec…   110MB
    <missing>      7 days ago       /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B
    <missing>      7 days ago       /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B
    <missing>      7 days ago       /bin/sh -c #(nop)  ARG LAUNCHPAD_BUILD_ARCH     0B
    <missing>      7 days ago       /bin/sh -c #(nop)  ARG RELEASE                  0B
    

    Note the “Add node” comment on the top line. This layer contains the Node.js install you just made.

  7. To prove your image has Node installed, you can start a new container using this new image:

    $ docker run node-base node -e "console.log('Hello again')"
    

    With that, you should get a “Hello again” output in the terminal, showing Node was installed and working.

  8. Now that you’re done creating your base image, you can remove that container:

    $ docker rm -f base-container
    

Base image definition

A base image is a foundation for building other images. It's possible to use any images as a base image. However, some images are intentionally created as building blocks, providing a foundation or starting point for an application.

In this example, you probably won’t deploy this node-base image, as it doesn’t actually do anything yet. But it’s a base you can use for other builds.

Build an app image

Now that you have a base image, you can extend that image to build additional images.

  1. Start a new container using the newly created node-base image:

    $ docker run --name=app-container -ti node-base
    
  2. Inside of this container, run the following command to create a Node program:

    $ echo 'console.log("Hello from an app")' > app.js
    

    To run this Node program, you can use the following command and see the message printed on the screen:

    $ node app.js
    
  3. In another terminal, run the following command to save this container’s changes as a new image:

    $ docker container commit -c "CMD node app.js" -m "Add app" app-container sample-app
    

    This command not only creates a new image named sample-app, but also adds additional configuration to the image to set the default command when starting a container. In this case, you are setting it to automatically run node app.js.

  4. In a terminal outside of the container, run the following command to view the updated layers:

    $ docker image history sample-app
    

    You’ll then see output that looks like the following. Note the top layer comment has “Add app” and the next layer has “Add node”:

    IMAGE          CREATED              CREATED BY                                      SIZE      COMMENT
    c1502e2ec875   About a minute ago   /bin/bash                                       33B       Add app
    5310da79c50a   4 minutes ago        /bin/bash                                       126MB     Add node
    2b7cc08dcdbb   5 weeks ago          /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
    <missing>      5 weeks ago          /bin/sh -c #(nop) ADD file:07cdbabf782942af0…   69.2MB
    <missing>      5 weeks ago          /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B
    <missing>      5 weeks ago          /bin/sh -c #(nop)  LABEL org.opencontainers.…   0B
    <missing>      5 weeks ago          /bin/sh -c #(nop)  ARG LAUNCHPAD_BUILD_ARCH     0B
    <missing>      5 weeks ago          /bin/sh -c #(nop)  ARG RELEASE                  0B
    
  5. Finally, start a new container using the brand new image. Since you specified the default command, you can use the following command:

    $ docker run sample-app
    

    You should see your greeting appear in the terminal, coming from your Node program.

  6. Now that you’re done with your containers, you can remove them using the following command:

    $ docker rm -f app-container
    

Additional resources

If you’d like to dive deeper into the things you learned, check out the following resources:

Next steps

As hinted earlier, most image builds don’t use docker container commit. Instead, you’ll use a Dockerfile which automates these steps for you.

{{< button text="Writing a Dockerfile" url="writing-a-dockerfile" >}}

Using the build cache

{{< youtube-embed Ri6jMknjprY >}}

Explanation

Consider the following Dockerfile that you created for the getting-started app.

FROM node:22-alpine
WORKDIR /app
COPY . .
RUN yarn install --production
CMD ["node", "./src/index.js"]

When you run the docker build command to create a new image, Docker executes each instruction in your Dockerfile, creating a layer for each command and in the order specified. For each instruction, Docker checks whether it can reuse the instruction from a previous build. If it finds that you've already executed a similar instruction before, Docker doesn't need to redo it. Instead, it’ll use the cached result. This way, your build process becomes faster and more efficient, saving you valuable time and resources.

Using the build cache effectively lets you achieve faster builds by reusing results from previous builds and skipping unnecessary work. In order to maximize cache usage and avoid resource-intensive and time-consuming rebuilds, it's important to understand how cache invalidation works. Here are a few examples of situations that can cause cache to be invalidated:

When you're writing or editing a Dockerfile, keep an eye out for unnecessary cache misses to ensure that builds run as fast and efficiently as possible.

Try it out

In this hands-on guide, you will learn how to use the Docker build cache effectively for a Node.js application.

Build the application

  1. Download and install Docker Desktop.

  2. Open a terminal and clone this sample application.

    $ git clone https://github.com/dockersamples/todo-list-app
    
  3. Navigate into the todo-list-app directory:

    $ cd todo-list-app
    

    Inside this directory, you'll find a file named Dockerfile with the following content:

    FROM node:22-alpine
    WORKDIR /app
    COPY . .
    RUN yarn install --production
    EXPOSE 3000
    CMD ["node", "./src/index.js"]
    
  4. Execute the following command to build the Docker image:

    $ docker build .
    

    Here’s the result of the build process:

    [+] Building 20.0s (10/10) FINISHED
    

    The first line indicates that the entire build process took 20.0 seconds. The first build may take some time as it installs dependencies.

  5. Rebuild without making changes.

    Now, re-run the docker build command without making any change in the source code or Dockerfile as shown:

    $ docker build .
    

    Subsequent builds after the initial are faster due to the caching mechanism, as long as the commands and context remain unchanged. Docker caches the intermediate layers generated during the build process. When you rebuild the image without making any changes to the Dockerfile or the source code, Docker can reuse the cached layers, significantly speeding up the build process.

    [+] Building 1.0s (9/9) FINISHED                                                                            docker:desktop-linux
     => [internal] load build definition from Dockerfile                                                                        0.0s
     => => transferring dockerfile: 187B                                                                                        0.0s
     ...
     => [internal] load build context                                                                                           0.0s
     => => transferring context: 8.16kB                                                                                         0.0s
     => CACHED [2/4] WORKDIR /app                                                                                               0.0s
     => CACHED [3/4] COPY . .                                                                                                   0.0s
     => CACHED [4/4] RUN yarn install --production                                                                              0.0s
     => exporting to image                                                                                                      0.0s
     => => exporting layers                                                                                                     0.0s
     => => exporting manifest
    

    The subsequent build was completed in just 1.0 second by leveraging the cached layers. No need to repeat time-consuming steps like installing dependencies.

    Steps Description Time Taken (1st Run) Time Taken (2nd Run)
    1 Load build definition from Dockerfile 0.0 seconds 0.0 seconds
    2 Load metadata for docker.io/library/node:22-alpine 2.7 seconds 0.9 seconds
    3 Load .dockerignore 0.0 seconds 0.0 seconds
    4 Load build context

    (Context size: 4.60MB)

    0.1 seconds 0.0 seconds
    5 Set the working directory (WORKDIR) 0.1 seconds 0.0 seconds
    6 Copy the local code into the container 0.0 seconds 0.0 seconds
    7 Run yarn install --production 10.0 seconds 0.0 seconds
    8 Exporting layers 2.2 seconds 0.0 seconds
    9 Exporting the final image 3.0 seconds 0.0 seconds

    Going back to the docker image history output, you see that each command in the Dockerfile becomes a new layer in the image. You might remember that when you made a change to the image, the yarn dependencies had to be reinstalled. Is there a way to fix this? It doesn't make much sense to reinstall the same dependencies every time you build, right?

    To fix this, restructure your Dockerfile so that the dependency cache remains valid unless it really needs to be invalidated. For Node-based applications, dependencies are defined in the package.json file. You'll want to reinstall the dependencies if that file changes, but use cached dependencies if the file is unchanged. So, start by copying only that file first, then install the dependencies, and finally copy everything else. Then, you only need to recreate the yarn dependencies if there was a change to the package.json file.

  6. Update the Dockerfile to copy in the package.json file first, install dependencies, and then copy everything else in.

    FROM node:22-alpine
    WORKDIR /app
    COPY package.json yarn.lock ./
    RUN yarn install --production 
    COPY . . 
    EXPOSE 3000
    CMD ["node", "src/index.js"]
    
  7. Create a file named .dockerignore in the same folder as the Dockerfile with the following contents.

    node_modules
    
  8. Build the new image:

    $ docker build .
    

    You'll then see output similar to the following:

    [+] Building 16.1s (10/10) FINISHED
    => [internal] load build definition from Dockerfile                                               0.0s
    => => transferring dockerfile: 175B                                                               0.0s
    => [internal] load .dockerignore                                                                  0.0s
    => => transferring context: 2B                                                                    0.0s
    => [internal] load metadata for docker.io/library/node:22-alpine                                  0.0s
    => [internal] load build context                                                                  0.8s
    => => transferring context: 53.37MB                                                               0.8s
    => [1/5] FROM docker.io/library/node:22-alpine                                                    0.0s
    => CACHED [2/5] WORKDIR /app                                                                      0.0s
    => [3/5] COPY package.json yarn.lock ./                                                           0.2s
    => [4/5] RUN yarn install --production                                                           14.0s
    => [5/5] COPY . .                                                                                 0.5s
    => exporting to image                                                                             0.6s
    => => exporting layers                                                                            0.6s
    => => writing image     
    sha256:d6f819013566c54c50124ed94d5e66c452325327217f4f04399b45f94e37d25        0.0s
    => => naming to docker.io/library/node-app:2.0                                                 0.0s
    

    You'll see that all layers were rebuilt. Perfectly fine since you changed the Dockerfile quite a bit.

  9. Now, make a change to the src/static/index.html file (like change the title to say "The Awesome Todo App").

  10. Build the Docker image. This time, your output should look a little different.

    $ docker build -t node-app:3.0 .
    

    You'll then see output similar to the following:

    [+] Building 1.2s (10/10) FINISHED 
    => [internal] load build definition from Dockerfile                                               0.0s
    => => transferring dockerfile: 37B                                                                0.0s
    => [internal] load .dockerignore                                                                  0.0s
    => => transferring context: 2B                                                                    0.0s
    => [internal] load metadata for docker.io/library/node:22-alpine                                  0.0s 
    => [internal] load build context                                                                  0.2s
    => => transferring context: 450.43kB                                                              0.2s
    => [1/5] FROM docker.io/library/node:22-alpine                                                    0.0s
    => CACHED [2/5] WORKDIR /app                                                                      0.0s
    => CACHED [3/5] COPY package.json yarn.lock ./                                                    0.0s
    => CACHED [4/5] RUN yarn install --production                                                     0.0s
    => [5/5] COPY . .                                                                                 0.5s 
    => exporting to image                                                                             0.3s
    => => exporting layers                                                                            0.3s
    => => writing image     
    sha256:91790c87bcb096a83c2bd4eb512bc8b134c757cda0bdee4038187f98148e2eda       0.0s
    => => naming to docker.io/library/node-app:3.0                                                 0.0s
    

    First off, you should notice that the build was much faster. You'll see that several steps are using previously cached layers. That's good news; you're using the build cache. Pushing and pulling this image and updates to it will be much faster as well.

By following these optimization techniques, you can make your Docker builds faster and more efficient, leading to quicker iteration cycles and improved development productivity.

Additional resources

Next steps

Now that you understand how to use the Docker build cache effectively, you're ready to learn about Multi-stage builds.

{{< button text="Multi-stage builds" url="multi-stage-builds" >}}

Writing a Dockerfile

{{< youtube-embed Jx8zoIhiP4c >}}

Explanation

A Dockerfile is a text-based document that's used to create a container image. It provides instructions to the image builder on the commands to run, files to copy, startup command, and more.

As an example, the following Dockerfile would produce a ready-to-run Python application:

FROM python:3.13
WORKDIR /usr/local/app

# Install the application dependencies
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt

# Copy in the source code
COPY src ./src
EXPOSE 8080

# Setup an app user so the container doesn't run as the root user
RUN useradd app
USER app

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]

Common instructions

Some of the most common instructions in a Dockerfile include:

To read through all of the instructions or go into greater detail, check out the Dockerfile reference.

Try it out

Just as you saw with the previous example, a Dockerfile typically follows these steps:

  1. Determine your base image
  2. Install application dependencies
  3. Copy in any relevant source code and/or binaries
  4. Configure the final image

In this quick hands-on guide, you'll write a Dockerfile that builds a simple Node.js application. If you're not familiar with JavaScript-based applications, don't worry. It isn't necessary for following along with this guide.

Set up

Download this ZIP file and extract the contents into a directory on your machine.

If you'd rather not download a ZIP file, clone the https://github.com/docker/getting-started-todo-app project and checkout the build-image-from-scratch branch.

Creating the Dockerfile

Now that you have the project, you’re ready to create the Dockerfile.

  1. Download and install Docker Desktop.

  2. Examine the project.

    Explore the contents of getting-started-todo-app/app/. You'll notice that a Dockerfile already exists. It is a simple text file that you can open in any text or code editor.

  3. Delete the existing Dockerfile.

    For this exercise, you'll pretend you're starting from scratch and will create a new Dockerfile.

  4. Create a file named Dockerfile in the getting-started-todo-app/app/ folder.

    Dockerfile file extensions

    It's important to note that the Dockerfile has no file extension. Some editors will automatically add an extension to the file (or complain it doesn't have one).

  5. In the Dockerfile, define your base image by adding the following line:

    FROM node:22-alpine
    
  6. Now, define the working directory by using the WORKDIR instruction. This will specify where future commands will run and the directory files will be copied inside the container image.

    WORKDIR /app
    
  7. Copy all of the files from your project on your machine into the container image by using the COPY instruction:

    COPY . .
    
  8. Install the app's dependencies by using the yarn CLI and package manager. To do so, run a command using the RUN instruction:

    RUN yarn install --production
    
  9. Finally, specify the default command to run by using the CMD instruction:

    CMD ["node", "./src/index.js"]
    

    And with that, you should have the following Dockerfile:

    FROM node:22-alpine
    WORKDIR /app
    COPY . .
    RUN yarn install --production
    CMD ["node", "./src/index.js"]
    

This Dockerfile isn't production-ready yet

It's important to note that this Dockerfile is not following all of the best practices yet (by design). It will build the app, but the builds won't be as fast, or the images as secure, as they could be.

Keep reading to learn more about how to make the image maximize the build cache, run as a non-root user, and multi-stage builds.

Additional resources

To learn more about writing a Dockerfile, visit the following resources:

Next steps

Now that you have created a Dockerfile and learned the basics, it's time to learn about building, tagging, and pushing the images.

{{< button text="Build, tag and publish the Image" url="build-tag-and-publish-an-image" >}}

Multi-container applications

{{< youtube-embed 1jUwR6F9hvM >}}

Explanation

Starting up a single-container application is easy. For example, a Python script that performs a specific data processing task runs within a container with all its dependencies. Similarly, a Node.js application serving a static website with a small API endpoint can be effectively containerized with all its necessary libraries and dependencies. However, as applications grow in size, managing them as individual containers becomes more difficult.

Imagine the data processing Python script needs to connect to a database. Suddenly, you're now managing not just the script but also a database server within the same container. If the script requires user logins, you'll need an authentication mechanism, further bloating the container size.

One best practice for containers is that each container should do one thing and do it well. While there are exceptions to this rule, avoid the tendency to have one container do multiple things.

Now you might ask, "Do I need to run these containers separately? If I run them separately, how shall I connect them all together?"

While docker run is a convenient tool for launching containers, it becomes difficult to manage a growing application stack with it. Here's why:

That's where Docker Compose comes to the rescue.

Docker Compose defines your entire multi-container application in a single YAML file called compose.yml. This file specifies configurations for all your containers, their dependencies, environment variables, and even volumes and networks. With Docker Compose:

By leveraging Docker Compose for running multi-container setups, you can build complex applications with modularity, scalability, and consistency at their core.

Try it out

In this hands-on guide, you'll first see how to build and run a counter web application based on Node.js, an Nginx reverse proxy, and a Redis database using the docker run commands. You’ll also see how you can simplify the entire deployment process using Docker Compose.

Set up

  1. Get the sample application. If you have Git, you can clone the repository for the sample application. Otherwise, you can download the sample application. Choose one of the following options.

    {{< tabs >}} {{< tab name="Clone with git" >}}

    Use the following command in a terminal to clone the sample application repository.

    $ git clone https://github.com/dockersamples/nginx-node-redis
    

    Navigate into the nginx-node-redis directory:

    $ cd nginx-node-redis
    

    Inside this directory, you'll find two sub-directories - nginx and web.

    {{< /tab >}} {{< tab name="Download" >}}

    Download the source and extract it.

    {{< button url="https://github.com/dockersamples/nginx-node-redis/archive/refs/heads/main.zip" text="Download the source" >}}

    Navigate into the nginx-node-redis-main directory:

    $ cd nginx-node-redis-main
    

    Inside this directory, you'll find two sub-directories - nginx and web.

    {{< /tab >}} {{< /tabs >}}

  2. Download and install Docker Desktop.

Build the images

  1. Navigate into the nginx directory to build the image by running the following command:

    $ docker build -t nginx .
    
  2. Navigate into the web directory and run the following command to build the first web image:

    $ docker build -t web .
    

Run the containers

  1. Before you can run a multi-container application, you need to create a network for them all to communicate through. You can do so using the docker network create command:

    $ docker network create sample-app
    
  2. Start the Redis container by running the following command, which will attach it to the previously created network and create a network alias (useful for DNS lookups):

    $ docker run -d  --name redis --network sample-app --network-alias redis redis
    
  3. Start the first web container by running the following command:

    $ docker run -d --name web1 -h web1 --network sample-app --network-alias web1 web
    
  4. Start the second web container by running the following:

    $ docker run -d --name web2 -h web2 --network sample-app --network-alias web2 web
    
  5. Start the Nginx container by running the following command:

    $ docker run -d --name nginx --network sample-app  -p 80:80 nginx
    

    [!NOTE]

    Nginx is typically used as a reverse proxy for web applications, routing traffic to backend servers. In this case, it routes to the Node.js backend containers (web1 or web2).

  6. Verify the containers are up by running the following command:

    $ docker ps
    

    You will see output like the following:

    CONTAINER ID   IMAGE     COMMAND                  CREATED              STATUS              PORTS                NAMES
    2cf7c484c144   nginx     "/docker-entrypoint.…"   9 seconds ago        Up 8 seconds        0.0.0.0:80->80/tcp   nginx
    7a070c9ffeaa   web       "docker-entrypoint.s…"   19 seconds ago       Up 18 seconds                            web2
    6dc6d4e60aaf   web       "docker-entrypoint.s…"   34 seconds ago       Up 33 seconds                            web1
    008e0ecf4f36   redis     "docker-entrypoint.s…"   About a minute ago   Up About a minute   6379/tcp             redis
    
  7. If you look at the Docker Desktop Dashboard, you can see the containers and dive deeper into their configuration.

    A screenshot of the Docker Desktop Dashboard showing multi-container applications

  8. With everything up and running, you can open http://localhost in your browser to see the site. Refresh the page several times to see the host that’s handling the request and the total number of requests:

    web2: Number of visits is: 9
    web1: Number of visits is: 10
    web2: Number of visits is: 11
    web1: Number of visits is: 12
    

    [!NOTE]

    You might have noticed that Nginx, acting as a reverse proxy, likely distributes incoming requests in a round-robin fashion between the two backend containers. This means each request might be directed to a different container (web1 and web2) on a rotating basis. The output shows consecutive increments for both the web1 and web2 containers and the actual counter value stored in Redis is updated only after the response is sent back to the client.

  9. You can use the Docker Desktop Dashboard to remove the containers by selecting the containers and selecting the Delete button.

    A screenshot of Docker Desktop Dashboard showing how to delete the multi-container applications

Simplify the deployment using Docker Compose

Docker Compose provides a structured and streamlined approach for managing multi-container deployments. As stated earlier, with Docker Compose, you don’t need to run multiple docker run commands. All you need to do is define your entire multi-container application in a single YAML file called compose.yml. Let’s see how it works.

Navigate to the root of the project directory. Inside this directory, you'll find a file named compose.yml. This YAML file is where all the magic happens. It defines all the services that make up your application, along with their configurations. Each service specifies its image, ports, volumes, networks, and any other settings necessary for its functionality.

  1. Use the docker compose up command to start the application:

    $ docker compose up -d --build
    

    When you run this command, you should see output similar to the following:

     ✔ Network nginx-node-redis_default   Created                                                                                                   0.0s
     ✔ Container nginx-node-redis-web2-1  Created                                                                                                   0.1s
     ✔ Container nginx-node-redis-web1-1  Created                                                                                                   0.1s
     ✔ Container nginx-node-redis-redis-1 Created                                                                                                   0.1s
     ✔ Container nginx-node-redis-nginx-1 Created   
    
    
  2. If you look at the Docker Desktop Dashboard, you can see the containers and dive deeper into their configuration.

    A screenshot of the Docker Desktop Dashboard showing the containers of the application stack deployed using Docker Compose

  3. Alternatively, you can use the Docker Desktop Dashboard to remove the containers by selecting the application stack and selecting the Delete button.

    A screenshot of Docker Desktop Dashboard that shows how to remove the containers that you deployed using Docker Compose

In this guide, you learned how easy it is to use Docker Compose to start and stop a multi-container application compared to docker run which is error-prone and difficult to manage.

Additional resources

Overriding container defaults

{{< youtube-embed PFszWK3BB8I >}}

Explanation

When a Docker container starts, it executes an application or command. The container gets this executable (script or file) from its image’s configuration. Containers come with default settings that usually work well, but you can change them if needed. These adjustments help the container's program run exactly how you want it to.

For example, if you have an existing database container that listens on the standard port and you want to run a new instance of the same database container, then you might want to change the port settings the new container listens on so that it doesn’t conflict with the existing container. Sometimes you might want to increase the memory available to the container if the program needs more resources to handle a heavy workload or set the environment variables to provide specific configuration details the program needs to function properly.

The docker run command offers a powerful way to override these defaults and tailor the container's behavior to your liking. The command offers several flags that let you to customize container behavior on the fly.

Here's a few ways you can achieve this.

Overriding the network ports

Sometimes you might want to use separate database instances for development and testing purposes. Running these database instances on the same port might conflict. You can use the -p option in docker run to map container ports to host ports, allowing you to run the multiple instances of the container without any conflict.

$ docker run -d -p HOST_PORT:CONTAINER_PORT postgres

Setting environment variables

This option sets an environment variable foo inside the container with the value bar.

$ docker run -e foo=bar postgres env

You will see output like the following:

HOSTNAME=2042f2e6ebe4
foo=bar

[!TIP]

The .env file acts as a convenient way to set environment variables for your Docker containers without cluttering your command line with numerous -e flags. To use a .env file, you can pass --env-file option with the docker run command.

$ docker run --env-file .env postgres env

Restricting the container to consume the resources

You can use the --memory and --cpus flags with the docker run command to restrict how much CPU and memory a container can use. For example, you can set a memory limit for the Python API container, preventing it from consuming excessive resources on your host. Here's the command:

$ docker run -e POSTGRES_PASSWORD=secret --memory="512m" --cpus="0.5" postgres

This command limits container memory usage to 512 MB and defines the CPU quota of 0.5 for half a core.

Monitor the real-time resource usage

You can use the docker stats command to monitor the real-time resource usage of running containers. This helps you understand whether the allocated resources are sufficient or need adjustment.

By effectively using these docker run flags, you can tailor your containerized application's behavior to fit your specific requirements.

Try it out

In this hands-on guide, you'll see how to use the docker run command to override the container defaults.

  1. Download and install Docker Desktop.

Run multiple instances of the Postgres database

  1. Start a container using the Postgres image with the following command:

    $ docker run -d -e POSTGRES_PASSWORD=secret -p 5432:5432 postgres
    

    This will start the Postgres database in the background, listening on the standard container port 5432 and mapped to port 5432 on the host machine.

  2. Start a second Postgres container mapped to a different port.

    $ docker run -d -e POSTGRES_PASSWORD=secret -p 5433:5432 postgres
    

    This will start another Postgres container in the background, listening on the standard postgres port 5432 in the container, but mapped to port 5433 on the host machine. You override the host port just to ensure that this new container doesn't conflict with the existing running container.

  3. Verify that both containers are running by going to the Containers view in the Docker Desktop Dashboard.

    A screenshot of the Docker Desktop Dashboard showing the running instances of Postgres containers

Run Postgres container in a controlled network

By default, containers automatically connect to a special network called a bridge network when you run them. This bridge network acts like a virtual bridge, allowing containers on the same host to communicate with each other while keeping them isolated from the outside world and other hosts. It's a convenient starting point for most container interactions. However, for specific scenarios, you might want more control over the network configuration.

Here's where the custom network comes in. You create a custom network by passing --network flag with the docker run command. All containers without a --network flag are attached to the default bridge network.

Follow the steps to see how to connect a Postgres container to a custom network.

  1. Create a new custom network by using the following command:

    $ docker network create mynetwork
    
  2. Verify the network by running the following command:

    $ docker network ls
    

    This command lists all networks, including the newly created "mynetwork".

  3. Connect Postgres to the custom network by using the following command:

    $ docker run -d -e POSTGRES_PASSWORD=secret -p 5434:5432 --network mynetwork postgres
    

    This will start Postgres container in the background, mapped to the host port 5434 and attached to the mynetwork network. You passed the --network parameter to override the container default by connecting the container to custom Docker network for better isolation and communication with other containers. You can use docker network inspect command to see if the container is tied to this new bridge network.

    Key difference between default bridge and custom networks

    1. DNS resolution: By default, containers connected to the default bridge network can communicate with each other, but only by IP address. (unless you use --link option which is considered legacy). It is not recommended for production use due to the various technical shortcomings. On a custom network, containers can resolve each other by name or alias.
    2. Isolation: All containers without a --network specified are attached to the default bridge network, hence can be a risk, as unrelated containers are then able to communicate. Using a custom network provides a scoped network in which only containers attached to that network are able to communicate, hence providing better isolation.

Manage the resources

By default, containers are not limited in their resource usage. However, on shared systems, it's crucial to manage resources effectively. It's important not to let a running container consume too much of the host machine's memory.

This is where the docker run command shines again. It offers flags like --memory and --cpus to restrict how much CPU and memory a container can use.

$ docker run -d -e POSTGRES_PASSWORD=secret --memory="512m" --cpus=".5" postgres

The --cpus flag specifies the CPU quota for the container. Here, it's set to half a CPU core (0.5) whereas the --memory flag specifies the memory limit for the container. In this case, it's set to 512 MB.

Override the default CMD and ENTRYPOINT in Docker Compose

Sometimes, you might need to override the default commands (CMD) or entry points (ENTRYPOINT) defined in a Docker image, especially when using Docker Compose.

  1. Create a compose.yml file with the following content:

    services:
      postgres:
        image: postgres:18
        entrypoint: ["docker-entrypoint.sh", "postgres"]
        command: ["-h", "localhost", "-p", "5432"]
        environment:
          POSTGRES_PASSWORD: secret 
    

    The Compose file defines a service named postgres that uses the official Postgres image, sets an entrypoint script, and starts the container with password authentication.

  2. Bring up the service by running the following command:

    $ docker compose up -d
    

    This command starts the Postgres service defined in the Docker Compose file.

  3. Verify the authentication with Docker Desktop Dashboard.

    Open the Docker Desktop Dashboard, select the Postgres container and select Exec to enter into the container shell. You can type the following command to connect to the Postgres database:

    # psql -U postgres
    

    A screenshot of the Docker Desktop Dashboard selecting the Postgres container and entering into its shell using EXEC button

    [!NOTE]

    The PostgreSQL image sets up trust authentication locally so you may notice a password isn't required when connecting from localhost (inside the same container). However, a password will be required if connecting from a different host/container.

Override the default CMD and ENTRYPOINT with docker run

You can also override defaults directly using the docker run command with the following command:

$ docker run -e POSTGRES_PASSWORD=secret postgres docker-entrypoint.sh -h localhost -p 5432

This command runs a Postgres container, sets an environment variable for password authentication, overrides the default startup commands and configures hostname and port mapping.

Additional resources

Next steps

Now that you have learned about overriding container defaults, it's time to learn how to persist container data.

{{< button text="Persisting container data" url="persisting-container-data" >}}

Persisting container data

{{< youtube-embed 10_2BjqB_Ls >}}

Explanation

When a container starts, it uses the files and configuration provided by the image. Each container is able to create, modify, and delete files and does so without affecting any other containers. When the container is deleted, these file changes are also deleted.

While this ephemeral nature of containers is great, it poses a challenge when you want to persist the data. For example, if you restart a database container, you might not want to start with an empty database. So, how do you persist files?

Container volumes

Volumes are a storage mechanism that provide the ability to persist data beyond the lifecycle of an individual container. Think of it like providing a shortcut or symlink from inside the container to outside the container.

As an example, imagine you create a volume named log-data.

$ docker volume create log-data

When starting a container with the following command, the volume will be mounted (or attached) into the container at /logs:

$ docker run -d -p 80:80 -v log-data:/logs docker/welcome-to-docker

If the volume log-data doesn't exist, Docker will automatically create it for you.

When the container runs, all files it writes into the /logs folder will be saved in this volume, outside of the container. If you delete the container and start a new container using the same volume, the files will still be there.

Sharing files using volumes

You can attach the same volume to multiple containers to share files between containers. This might be helpful in scenarios such as log aggregation, data pipelines, or other event-driven applications.

Managing volumes

Volumes have their own lifecycle beyond that of containers and can grow quite large depending on the type of data and applications you’re using. The following commands will be helpful to manage volumes:

Try it out

In this guide, you'll practice creating and using volumes to persist data created by a Postgres container. When the database runs, it stores files into the /var/lib/postgresql directory. By attaching the volume here, you will be able to restart the container multiple times while keeping the data.

Use volumes

  1. Download and install Docker Desktop.

  2. Start a container using the Postgres image with the following command:

    $ docker run --name=db -e POSTGRES_PASSWORD=secret -d -v postgres_data:/var/lib/postgresql postgres:18
    

    This will start the database in the background, configure it with a password, and attach a volume to the directory PostgreSQL will persist the database files.

  3. Connect to the database by using the following command:

    $ docker exec -ti db psql -U postgres
    
  4. In the PostgreSQL command line, run the following to create a database table and insert two records:

    CREATE TABLE tasks (
        id SERIAL PRIMARY KEY,
        description VARCHAR(100)
    );
    INSERT INTO tasks (description) VALUES ('Finish work'), ('Have fun');
    
  5. Verify the data is in the database by running the following in the PostgreSQL command line:

    SELECT * FROM tasks;
    

    You should get output that looks like the following:

     id | description
    ----+-------------
      1 | Finish work
      2 | Have fun
    (2 rows)
    
  6. Exit out of the PostgreSQL shell by running the following command:

    \q
    
  7. Stop and remove the database container. Remember that, even though the container has been deleted, the data is persisted in the postgres_data volume.

    $ docker stop db
    $ docker rm db
    
  8. Start a new container by running the following command, attaching the same volume with the persisted data:

    $ docker run --name=new-db -d -v postgres_data:/var/lib/postgresql postgres:18
    

    You might have noticed that the POSTGRES_PASSWORD environment variable has been omitted. That’s because that variable is only used when bootstrapping a new database.

  9. Verify the database still has the records by running the following command:

    $ docker exec -ti new-db psql -U postgres -c "SELECT * FROM tasks"
    

View volume contents

The Docker Desktop Dashboard provides the ability to view the contents of any volume, as well as the ability to export, import, empty, delete and clone volumes.

  1. Open the Docker Desktop Dashboard and navigate to the Volumes view. In this view, you should see the postgres_data volume.

  2. Select the postgres_data volume’s name.

  3. The Stored Data tab shows the contents of the volume and provides the ability to navigate the files. The Container in-use tab displays the name of the container using the volume, the image name, the port number used by the container, and the target. A target is a path inside a container that gives access to the files in the volume. The Exports tab lets you export the volume. Double-clicking on a file will let you see the contents and make changes.

  4. Right-click on any file to save it or delete it.

Remove volumes

Before removing a volume, it must not be attached to any containers. If you haven’t removed the previous container, do so with the following command (the -f will stop the container first and then remove it):

$ docker rm -f new-db

There are a few methods to remove volumes, including the following:

Additional resources

The following resources will help you learn more about volumes:

Next steps

Now that you have learned about persisting container data, it’s time to learn about sharing local files with containers.

{{< button text="Sharing local files with containers" url="sharing-local-files" >}}

Publishing and exposing ports

{{< youtube-embed 9JnqOmJ96ds >}}

Explanation

If you've been following the guides so far, you understand that containers provide isolated processes for each component of your application. Each component - a React frontend, a Python API, and a Postgres database - runs in its own sandbox environment, completely isolated from everything else on your host machine. This isolation is great for security and managing dependencies, but it also means you can’t access them directly. For example, you can’t access the web app in your browser.

That’s where port publishing comes in.

Publishing ports

Publishing a port provides the ability to break through a little bit of networking isolation by setting up a forwarding rule. As an example, you can indicate that requests on your host’s port 8080 should be forwarded to the container’s port 80. Publishing ports happens during container creation using the -p (or --publish) flag with docker run. The syntax is:

$ docker run -d -p HOST_PORT:CONTAINER_PORT nginx

For example, to publish the container's port 80 to host port 8080:

$ docker run -d -p 8080:80 nginx

Now, any traffic sent to port 8080 on your host machine will be forwarded to port 80 within the container.

[!IMPORTANT]

When a port is published, it's published to all network interfaces by default. This means any traffic that reaches your machine can access the published application. Be mindful of publishing databases or any sensitive information. Learn more about published ports here.

Publishing on a specific host IP

By default, published ports listen on all host interfaces (0.0.0.0 / ::). You can bind to a single address by putting the IP first in the publish string:

$ docker run -d -p 127.0.0.1:8080:80 nginx

The full form is HOST_IP:HOST_PORT:CONTAINER_PORT.

In Compose, the same extended form works under ports:

services:
  app:
    image: docker/welcome-to-docker
    ports:
      - "127.0.0.1:8080:80"

Publishing to ephemeral ports

At times, you may want to simply publish the port but don’t care which host port is used. In these cases, you can let Docker pick the port for you. To do so, simply omit the HOST_PORT configuration.

For example, the following command will publish the container’s port 80 onto an ephemeral port on the host:

$ docker run -p 80 nginx

Once the container is running, using docker ps will show you the port that was chosen:

docker ps
CONTAINER ID   IMAGE         COMMAND                  CREATED          STATUS          PORTS                    NAMES
a527355c9c53   nginx         "/docker-entrypoint.…"   4 seconds ago    Up 3 seconds    0.0.0.0:54772->80/tcp    romantic_williamson

In this example, the app is exposed on the host at port 54772.

Publishing all ports

When creating a container image, the EXPOSE instruction is used to indicate the packaged application will use the specified port. These ports aren't published by default.

With the -P or --publish-all flag, you can automatically publish all exposed ports to ephemeral ports. This is quite useful when you’re trying to avoid port conflicts in development or testing environments.

For example, the following command will publish all of the exposed ports configured by the image:

$ docker run -P nginx

Try it out

In this hands-on guide, you'll learn how to publish container ports using both the CLI and Docker Compose for deploying a web application.

Use the Docker CLI

In this step, you will run a container and publish its port using the Docker CLI.

  1. Download and install Docker Desktop.

  2. In a terminal, run the following command to start a new container:

    $ docker run -d -p 8080:80 docker/welcome-to-docker
    

    The first 8080 refers to the host port. This is the port on your local machine that will be used to access the application running inside the container. The second 80 refers to the container port. This is the port that the application inside the container listens on for incoming connections. Hence, the command binds to port 8080 of the host to port 80 on the container system.

  3. Verify the published port by going to the Containers view of the Docker Desktop Dashboard.

    A screenshot of Docker Desktop Dashboard showing the published port

  4. Open the website by either selecting the link in the Port(s) column of your container or visiting http://localhost:8080 in your browser.

    A screenshot of the landing page of the Nginx web server running in a container

Use Docker Compose

This example will launch the same application using Docker Compose:

  1. Create a new directory and inside that directory, create a compose.yaml file with the following contents:

    services:
      app:
        image: docker/welcome-to-docker
        ports:
          - 8080:80
    

    The ports configuration accepts a few different forms of syntax for the port definition. In this case, you’re using the same HOST_PORT:CONTAINER_PORT used in the docker run command.

  2. Open a terminal and navigate to the directory you created in the previous step.

  3. Use the docker compose up command to start the application.

  4. Open your browser to http://localhost:8080.

Additional resources

If you’d like to dive in deeper on this topic, be sure to check out the following resources:

Next steps

Now that you understand how to publish and expose ports, you're ready to learn how to override the container defaults using the docker run command.

{{< button text="Overriding container defaults" url="overriding-container-defaults" >}}

Sharing local files with containers

{{< youtube-embed 2dAzsVg3Dek >}}

Explanation

Each container has everything it needs to function with no reliance on any pre-installed dependencies on the host machine. Since containers run in isolation, they have minimal influence on the host and other containers. This isolation has a major benefit: containers minimize conflicts with the host system and other containers. However, this isolation also means containers can't directly access data on the host machine by default.

Consider a scenario where you have a web application container that requires access to configuration settings stored in a file on your host system. This file may contain sensitive data such as database credentials or API keys. Storing such sensitive information directly within the container image poses security risks, especially during image sharing. To address this challenge, Docker offers storage options that bridge the gap between container isolation and your host machine's data.

Docker offers two primary storage options for persisting data and sharing files between the host machine and containers: volumes and bind mounts.

Volume versus bind mounts

If you want to ensure that data generated or modified inside the container persists even after the container stops running, you would opt for a volume. See Persisting container data to learn more about volumes and their use cases.

If you have specific files or directories on your host system that you want to directly share with your container, like configuration files or development code, then you would use a bind mount. It's like opening a direct portal between your host and container for sharing. Bind mounts are ideal for development environments where real-time file access and sharing between the host and container are crucial.

Sharing files between a host and container

Both -v (or --volume) and --mount flags used with the docker run command let you share files or directories between your local machine (host) and a Docker container. However, there are some key differences in their behavior and usage.

The -v flag is simpler and more convenient for basic volume or bind mount operations. If the host location doesn’t exist when using -v or --volume, a directory will be automatically created.

Imagine you're a developer working on a project. You have a source directory on your development machine where your code resides. When you compile or build your code, the generated artifacts (compiled code, executables, images, etc.) are saved in a separate subdirectory within your source directory. In the following examples, this subdirectory is /HOST/PATH. Now you want these build artifacts to be accessible within a Docker container running your application. Additionally, you want the container to automatically access the latest build artifacts whenever you rebuild your code.

Here's a way to use docker run to start a container using a bind mount and map it to the container file location.

$ docker run -v /HOST/PATH:/CONTAINER/PATH -it nginx

The --mount flag offers more advanced features and granular control, making it suitable for complex mount scenarios or production deployments. By default, if you use --mount to bind-mount a file or directory that doesn't yet exist on the Docker host, the docker run command doesn't automatically create it for you but generates an error.

$ docker run --mount type=bind,source=/HOST/PATH,target=/CONTAINER/PATH,readonly nginx

[!NOTE]

Docker recommends using the --mount syntax instead of -v. It provides better control over the mounting process and avoids potential issues with missing directories.

File permissions for Docker access to host files

When using bind mounts, it's crucial to ensure that Docker has the necessary permissions to access the host directory. To grant read/write access, you can use the :ro flag (read-only) or :rw (read-write) with the -v or --mount flag during container creation. For example, the following command grants read-write access permission.

$ docker run -v HOST-DIRECTORY:/CONTAINER-DIRECTORY:rw nginx

Read-only bind mounts let the container access the mounted files on the host for reading, but it can't change or delete the files. With read-write bind mounts, containers can modify or delete mounted files, and these changes or deletions will also be reflected on the host system. Read-only bind mounts ensures that files on the host can't be accidentally modified or deleted by a container.

Synchronized File Share

As your codebase grows larger, traditional methods of file sharing like bind mounts may become inefficient or slow, especially in development environments where frequent access to files is necessary. Synchronized file shares improve bind mount performance by leveraging synchronized filesystem caches. This optimization ensures that file access between the host and virtual machine (VM) is fast and efficient.

Try it out

In this hands-on guide, you’ll practice how to create and use a bind mount to share files between a host and a container.

Run a container

  1. Download and install Docker Desktop.

  2. Start a container using the httpd image with the following command:

    $ docker run -d -p 8080:80 --name my_site httpd:2.4
    

    This will start the httpd service in the background, and publish the webpage to port 8080 on the host.

  3. Open the browser and access http://localhost:8080 or use the curl command to verify if it's working fine or not.

    $ curl localhost:8080
    

Use a bind mount

Using a bind mount, you can map the configuration file on your host computer to a specific location within the container. In this example, you’ll see how to change the look and feel of the webpage by using bind mount:

  1. Delete the existing container by using the Docker Desktop Dashboard:

    A screenshot of Docker Desktop Dashboard showing how to delete the httpd container

  2. Create a new directory called public_html on your host system.

    $ mkdir public_html
    
  3. Navigate into the newly created directory public_html and create a file called index.html with the following content. This is a basic HTML document that creates a simple webpage that welcomes you with a friendly whale.

    <!DOCTYPE html>
    <html lang="en">
    <head>
    <meta charset="UTF-8">
    <title> My Website with a Whale & Docker!</title>
    </head>
    <body>
    <h1>Whalecome!!</h1>
    <p>Look! There's a friendly whale greeting you!</p>
    <pre id="docker-art">
       ##         .
      ## ## ##        ==
     ## ## ## ## ##    ===
     /"""""""""""""""""\___/ ===
    {                       /  ===-
    \______ O           __/
    \    \         __/
     \____\_______/
    
    Hello from Docker!
    </pre>
    </body>
    </html>
    
  4. It's time to run the container. The --mount and -v examples produce the same result. You can't run them both unless you remove the my_site container after running the first one.

    {{< tabs >}} {{< tab name="-v" >}}

    $ docker run -d --name my_site -p 8080:80 -v .:/usr/local/apache2/htdocs/ httpd:2.4
    

    {{< /tab >}} {{< tab name="--mount" >}}

    $ docker run -d --name my_site -p 8080:80 --mount type=bind,source=./,target=/usr/local/apache2/htdocs/ httpd:2.4
    

    {{< /tab >}} {{< /tabs >}}

    [!TIP]
    When using the -v or --mount flag in Windows PowerShell, you need to provide the absolute path to your directory instead of just ./. This is because PowerShell handles relative paths differently from bash (commonly used in Mac and Linux environments).

    With everything now up and running, you should be able to access the site via http://localhost:8080 and find a new webpage that welcomes you with a friendly whale.

Access the file on the Docker Desktop Dashboard

  1. You can view the mounted files inside a container by selecting the container's Files tab and then selecting a file inside the /usr/local/apache2/htdocs/ directory. Then, select Open file editor.

    A screenshot of Docker Desktop Dashboard showing the mounted files inside the container

  2. Delete the file on the host and verify the file is also deleted in the container. You will find that the files no longer exist under Files in the Docker Desktop Dashboard.

    A screenshot of Docker Desktop Dashboard showing the deleted files inside the container

  3. Recreate the HTML file on the host system and see that file re-appears under the Files tab under Containers on the Docker Desktop Dashboard. By now, you will be able to access the site too.

Stop your container

The container continues to run until you stop it.

  1. Go to the Containers view in the Docker Desktop Dashboard.

  2. Locate the container you'd like to stop.

  3. Select the Stop action in the Actions column.

Additional resources

The following resources will help you learn more about bind mounts:

Next steps

Now that you have learned about sharing local files with containers, it’s time to learn about multi-container applications.

{{< button text="Multi-container applications" url="Multi-container applications" >}}

What is a container?

{{< youtube-embed W1kWqFkiu7k >}}

Explanation

Imagine you're developing a killer web app that has three main components - a React frontend, a Python API, and a PostgreSQL database. If you wanted to work on this project, you'd have to install Node, Python, and PostgreSQL.

How do you make sure you have the same versions as the other developers on your team? Or your CI/CD system? Or what's used in production?

How do you ensure the version of Python (or Node or the database) your app needs isn't affected by what's already on your machine? How do you manage potential conflicts?

Enter containers!

What is a container? Simply put, containers are isolated processes for each of your app's components. Each component - the frontend React app, the Python API engine, and the database - runs in its own isolated environment, completely isolated from everything else on your machine.

Here's what makes them awesome. Containers are:

Containers versus virtual machines (VMs)

Without getting too deep, a VM is an entire operating system with its own kernel, hardware drivers, programs, and applications. Spinning up a VM only to isolate a single application is a lot of overhead.

A container is simply an isolated process with all of the files it needs to run. If you run multiple containers, they all share the same kernel, allowing you to run more applications on less infrastructure.

Using VMs and containers together

Quite often, you will see containers and VMs used together. As an example, in a cloud environment, the provisioned machines are typically VMs. However, instead of provisioning one machine to run one application, a VM with a container runtime can run multiple containerized applications, increasing resource utilization and reducing costs.

Try it out

In this hands-on, you will see how to run a Docker container using the Docker Desktop GUI.

{{< tabs group=concept-usage persist=true >}} {{< tab name="Using the GUI" >}}

Use the following instructions to run a container.

  1. Open Docker Desktop and select the Search field on the top navigation bar.

  2. Specify welcome-to-docker in the search input and then select the Pull button.

    A screenshot of the Docker Desktop Dashboard showing the search result for welcome-to-docker Docker image

  3. Once the image is successfully pulled, select the Run button.

  4. Expand the Optional settings.

  5. In the Container name, specify welcome-to-docker.

  6. In the Host port, specify 8080.

    A screenshot of Docker Desktop Dashboard showing the container run dialog with welcome-to-docker typed in as the container name and 8080 specified as the port number

  7. Select Run to start your container.

Congratulations! You just ran your first container! 🎉

View your container

You can view all of your containers by going to the Containers view of the Docker Desktop Dashboard.

Screenshot of the container view of the Docker Desktop GUI showing the welcome-to-docker container running on the host port 8080

This container runs a web server that displays a simple website. When working with more complex projects, you'll run different parts in different containers. For example, you might run a different container for the frontend, backend, and database.

Access the frontend

When you launched the container, you exposed one of the container's ports onto your machine. Think of this as creating configuration to let you connect through the isolated environment of the container.

For this container, the frontend is accessible on port 8080. To open the website, select the link in the Port(s) column of your container or visit http://localhost:8080 in your browser.

Screenshot of the landing page coming from the running container

Explore your container

Docker Desktop lets you explore and interact with different aspects of your container. Try it out yourself.

  1. Go to the Containers view in the Docker Desktop Dashboard.

  2. Select your container.

  3. Select the Files tab to explore your container's isolated file system.

    Screenshot of the Docker Desktop Dashboard showing the files and directories inside a running container

Stop your container

The docker/welcome-to-docker container continues to run until you stop it.

  1. Go to the Containers view in the Docker Desktop Dashboard.

  2. Locate the container you'd like to stop.

  3. Select the Stop action in the Actions column.

    Screenshot of the Docker Desktop Dashboard with the welcome container selected and being prepared to stop

{{< /tab >}} {{< tab name="Using the CLI" >}}

Follow the instructions to run a container using the CLI:

  1. Open your CLI terminal and start a container by using the docker run command:

    $ docker run -d -p 8080:80 docker/welcome-to-docker
    

    The output from this command is the full container ID.

Congratulations! You just fired up your first container! 🎉

View your running containers

You can verify if the container is up and running by using the docker ps command:

docker ps

You will see output like the following:

 CONTAINER ID   IMAGE                      COMMAND                  CREATED          STATUS          PORTS                      NAMES
 a1f7a4bb3a27   docker/welcome-to-docker   "/docker-entrypoint.…"   11 seconds ago   Up 11 seconds   0.0.0.0:8080->80/tcp       gracious_keldysh

This container runs a web server that displays a simple website. When working with more complex projects, you'll run different parts in different containers. For example, a different container for the frontend, backend, and database.

[!TIP]

The docker ps command will show you only running containers. To view stopped containers, add the -a flag to list all containers: docker ps -a

Access the frontend

When you launched the container, you exposed one of the container's ports onto your machine. Think of this as creating configuration to let you to connect through the isolated environment of the container.

For this container, the frontend is accessible on port 8080. To open the website, select the link in the Port(s) column of your container or visit http://localhost:8080 in your browser.

Screenshot of the landing page of the Nginx web server, coming from the running container

Stop your container

The docker/welcome-to-docker container continues to run until you stop it. You can stop a container using the docker stop command.

  1. Run docker ps to get the ID of the container

  2. Provide the container ID or name to the docker stop command:

    docker stop <the-container-id>
    

[!TIP]

When referencing containers by ID, you don't need to provide the full ID. You only need to provide enough of the ID to make it unique. As an example, the previous container could be stopped by running the following command:

docker stop a1f

{{< /tab >}} {{< /tabs >}}

Additional resources

Next steps

Now that you have learned the basics of a Docker container, it's time to learn about Docker images.

{{< button text="What is an image?" url="what-is-an-image" >}}

What is a registry?

{{< youtube-embed 2WDl10Wv5rs >}}

Explanation

Now that you know what a container image is and how it works, you might wonder - where do you store these images?

Well, you can store your container images on your computer system, but what if you want to share them with your friends or use them on another machine? That's where the image registry comes in.

An image registry is a centralized location for storing and sharing your container images. It can be either public or private. Docker Hub is a public registry that anyone can use and is the default registry.

While Docker Hub is a popular option, there are many other available container registries available today, including Amazon Elastic Container Registry (ECR), Azure Container Registry (ACR), and Google Container Registry (GCR). You can even run your private registry on your local system or inside your organization. For example, Harbor, JFrog Artifactory, GitLab Container registry etc.

Registry vs. repository

While you're working with registries, you might hear the terms registry and repository as if they're interchangeable. Even though they're related, they're not quite the same thing.

A registry is a centralized location that stores and manages container images, whereas a repository is a collection of related container images within a registry. Think of it as a folder where you organize your images based on projects. Each repository contains one or more container images.

The following diagram shows the relationship between a registry, repositories, and images.

flowchart TB
  subgraph Registry
    subgraph A["Repository A"]
      A1["project-a:v1.0"]
      A2["project-a:v2.0"]
    end
    subgraph B["Repository B"]
      B1["project-b:v1.0"]
      B2["project-b:v1.1"]
      B3["project-b:v2.0"]
    end
  end

[!TIP]

A Docker Personal plan gives you one private repository and unlimited public repositories. To get unlimited private repositories, upgrade to the Docker Team plan.

Try it out

In this hands-on, you will learn how to build and push a Docker image to the Docker Hub repository.

Sign up for a free Docker account

  1. If you haven't created one yet, head over to the Docker Hub page to sign up for a new Docker account. Be sure to finish the verification steps sent to your email.

    Screenshot of the official Docker Hub page showing the Sign up page

    You can use your Google or GitHub account to authenticate.

Create your first repository

  1. Sign in to Docker Hub.

  2. Select the Create repository button in the top-right corner.

  3. Select your namespace (most likely your username) and enter docker-quickstart as the repository name.

    Screenshot of the Docker Hub page that shows how to create a public repository

  4. Set the visibility to Public.

  5. Select the Create button to create the repository.

That's it. You've successfully created your first repository. 🎉

This repository is empty right now. You'll now fix this by pushing an image to it.

Sign in with Docker Desktop

  1. Download and install Docker Desktop, if not already installed.
  2. In the Docker Desktop GUI, select the Sign in button in the top-right corner

Clone sample Node.js code

In order to create an image, you first need a project. To get you started quickly, you'll use a sample Node.js project found at github.com/dockersamples/helloworld-demo-node. This repository contains a pre-built Dockerfile necessary for building a Docker image.

Don't worry about the specifics of the Dockerfile, as you'll learn about that in later sections.

  1. Clone the GitHub repository using the following command:

    git clone https://github.com/dockersamples/helloworld-demo-node
    
  2. Navigate into the newly created directory.

    cd helloworld-demo-node
    
  3. Run the following command to build a Docker image, swapping out YOUR_DOCKER_USERNAME with your username.

    docker build -t <YOUR_DOCKER_USERNAME>/docker-quickstart .
    

    [!NOTE]

    Make sure you include the dot (.) at the end of the docker build command. This tells Docker where to find the Dockerfile.

  4. Run the following command to list the newly created Docker image:

    docker images
    

    You will see output like the following:

    REPOSITORY                                 TAG       IMAGE ID       CREATED         SIZE
    <YOUR_DOCKER_USERNAME>/docker-quickstart   latest    476de364f70e   2 minutes ago   170MB
    
  5. Start a container to test the image by running the following command (swap out the username with your own username):

    docker run -d -p 8080:8080 <YOUR_DOCKER_USERNAME>/docker-quickstart
    

    You can verify if the container is working by visiting http://localhost:8080 with your browser.

  6. Use the docker tag command to tag the Docker image. Docker tags allow you to label and version your images.

    docker tag <YOUR_DOCKER_USERNAME>/docker-quickstart <YOUR_DOCKER_USERNAME>/docker-quickstart:1.0
    
  7. Finally, it's time to push the newly built image to your Docker Hub repository by using the docker push command:

    docker push <YOUR_DOCKER_USERNAME>/docker-quickstart:1.0
    
  8. Open Docker Hub and navigate to your repository. Navigate to the Tags section and see your newly pushed image.

    Screenshot of the Docker Hub page that displays the newly added image tag

In this walkthrough, you signed up for a Docker account, created your first Docker Hub repository, and built, tagged, and pushed a container image to your Docker Hub repository.

Additional resources

Next steps

Now that you understand the basics of containers and images, you're ready to learn about Docker Compose.

{{< button text="What is Docker Compose?" url="what-is-Docker-Compose" >}}

What is an image?

{{< youtube-embed NyvT9REqLe4 >}}

Explanation

Seeing as a container is an isolated process, where does it get its files and configuration? How do you share those environments?

That's where container images come in. A container image is a standardized package that includes all of the files, binaries, libraries, and configurations to run a container.

For a PostgreSQL image, that image will package the database binaries, config files, and other dependencies. For a Python web app, it'll include the Python runtime, your app code, and all of its dependencies.

There are two important principles of images:

  1. Images are immutable. Once an image is created, it can't be modified. You can only make a new image or add changes on top of it.

  2. Container images are composed of layers. Each layer represents a set of file system changes that add, remove, or modify files.

These two principles let you to extend or add to existing images. For example, if you are building a Python app, you can start from the Python image and add additional layers to install your app's dependencies and add your code. This lets you focus on your app, rather than Python itself.

Finding images

Docker Hub is the default global marketplace for storing and distributing images. It has over 100,000 images created by developers that you can run locally. You can search for Docker Hub images and run them directly from Docker Desktop.

Docker Hub provides a variety of Docker-supported and endorsed images known as Docker Trusted Content. These provide fully managed services or great starters for your own images. These include:

For example, Redis and Memcached are a few popular ready-to-go Docker Official Images. You can download these images and have these services up and running in a matter of seconds. There are also base images, like the Node.js Docker image, that you can use as a starting point and add your own files and configurations. For production workloads requiring enhanced security, Docker Hardened Images offer minimal variants of popular images like Node.js, Python, and Go.

Try it out

{{< tabs group=concept-usage persist=true >}} {{< tab name="Using the GUI" >}}

In this hands-on, you will learn how to search and pull a container image using the Docker Desktop GUI.

Search for and download an image

  1. Open the Docker Desktop Dashboard and select the Images view in the left-hand navigation menu.

    A screenshot of the Docker Desktop Dashboard showing the image view on the left sidebar

  2. Select the Search images to run button. If you don't see it, select the global search bar at the top of the screen.

    A screenshot of the Docker Desktop Dashboard showing the search ta

  3. In the Search field, enter "welcome-to-docker". Once the search has completed, select the docker/welcome-to-docker image.

    A screenshot of the Docker Desktop Dashboard showing the search results for the docker/welcome-to-docker image

  4. Select Pull to download the image.

Learn about the image

Once you have an image downloaded, you can learn quite a few details about the image either through the GUI or the CLI.

  1. In the Docker Desktop Dashboard, select the Images view.

  2. Select the docker/welcome-to-docker image to open details about the image.

    A screenshot of the Docker Desktop Dashboard showing the images view with an arrow pointing to the docker/welcome-to-docker image

  3. The image details page presents you with information regarding the layers of the image, the packages and libraries installed in the image, and any discovered vulnerabilities.

    A screenshot of the image details view for the docker/welcome-to-docker image

{{< /tab >}}

{{< tab name="Using the CLI" >}}

Follow the instructions to search and pull a Docker image using CLI to view its layers.

Search for and download an image

  1. Open a terminal and search for images using the docker search command:

    docker search docker/welcome-to-docker
    

    You will see output like the following:

    NAME                       DESCRIPTION                                     STARS     OFFICIAL
    docker/welcome-to-docker   Docker image for new users getting started w…   20
    

    This output shows you information about relevant images available on Docker Hub.

  2. Pull the image using the docker pull command.

    docker pull docker/welcome-to-docker
    

    You will see output like the following:

    Using default tag: latest
    latest: Pulling from docker/welcome-to-docker
    579b34f0a95b: Download complete
    d11a451e6399: Download complete
    1c2214f9937c: Download complete
    b42a2f288f4d: Download complete
    54b19e12c655: Download complete
    1fb28e078240: Download complete
    94be7e780731: Download complete
    89578ce72c35: Download complete
    Digest: sha256:eedaff45e3c78538087bdd9dc7afafac7e110061bbdd836af4104b10f10ab693
    Status: Downloaded newer image for docker/welcome-to-docker:latest
    docker.io/docker/welcome-to-docker:latest
    

    Each of line represents a different downloaded layer of the image. Remember that each layer is a set of filesystem changes and provides functionality of the image.

Learn about the image

  1. List your downloaded images using the docker image ls command:

    docker image ls
    

    You will see output like the following:

    REPOSITORY                 TAG       IMAGE ID       CREATED        SIZE
    docker/welcome-to-docker   latest    eedaff45e3c7   4 months ago   29.7MB
    

    The command shows a list of Docker images currently available on your system. The docker/welcome-to-docker has a total size of approximately 29.7MB.

    Image size

    The image size represented here reflects the uncompressed size of the image, not the download size of the layers.

  2. List the image's layers using the docker image history command:

    docker image history docker/welcome-to-docker
    

    You will see output like the following:

    IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
    648f93a1ba7d   4 months ago   COPY /app/build /usr/share/nginx/html # buil…   1.6MB     buildkit.dockerfile.v0
    <missing>      5 months ago   /bin/sh -c #(nop)  CMD ["nginx" "-g" "daemon…   0B
    <missing>      5 months ago   /bin/sh -c #(nop)  STOPSIGNAL SIGQUIT           0B
    <missing>      5 months ago   /bin/sh -c #(nop)  EXPOSE 80                    0B
    <missing>      5 months ago   /bin/sh -c #(nop)  ENTRYPOINT ["/docker-entr…   0B
    <missing>      5 months ago   /bin/sh -c #(nop) COPY file:9e3b2b63db9f8fc7…   4.62kB
    <missing>      5 months ago   /bin/sh -c #(nop) COPY file:57846632accc8975…   3.02kB
    <missing>      5 months ago   /bin/sh -c #(nop) COPY file:3b1b9915b7dd898a…   298B
    <missing>      5 months ago   /bin/sh -c #(nop) COPY file:caec368f5a54f70a…   2.12kB
    <missing>      5 months ago   /bin/sh -c #(nop) COPY file:01e75c6dd0ce317d…   1.62kB
    <missing>      5 months ago   /bin/sh -c set -x     && addgroup -g 101 -S …   9.7MB
    <missing>      5 months ago   /bin/sh -c #(nop)  ENV PKG_RELEASE=1            0B
    <missing>      5 months ago   /bin/sh -c #(nop)  ENV NGINX_VERSION=1.25.3     0B
    <missing>      5 months ago   /bin/sh -c #(nop)  LABEL maintainer=NGINX Do…   0B
    <missing>      5 months ago   /bin/sh -c #(nop)  CMD ["/bin/sh"]              0B
    <missing>      5 months ago   /bin/sh -c #(nop) ADD file:ff3112828967e8004…   7.66MB
    

    This output shows you all of the layers, their sizes, and the command used to create the layer.

    Viewing the full command

    If you add the --no-trunc flag to the command, you will see the full command. Note that, since the output is in a table-like format, longer commands will cause the output to be very difficult to navigate.

{{< /tab >}} {{< /tabs >}}

In this walkthrough, you searched and pulled a Docker image. In addition to pulling a Docker image, you also learned about the layers of a Docker Image.

Additional resources

The following resources will help you learn more about exploring, finding, and building images:

Next steps

Now that you have learned the basics of images, it's time to learn about distributing images through registries.

{{< button text="What is a registry?" url="what-is-a-registry" >}}

What is Docker Compose?

{{< youtube-embed xhcUIK4fGtY >}}

Explanation

If you've been following the guides so far, you've been working with single container applications. But, now you're wanting to do something more complicated - run databases, message queues, caches, or a variety of other services. Do you install everything in a single container? Run multiple containers? If you run multiple, how do you connect them all together?

One best practice for containers is that each container should do one thing and do it well. While there are exceptions to this rule, avoid the tendency to have one container do multiple things.

You can use multiple docker run commands to start multiple containers. But, you'll soon realize you'll need to manage networks, all of the flags needed to connect containers to those networks, and more. And when you're done, cleanup is a little more complicated.

With Docker Compose, you can define all of your containers and their configurations in a single YAML file. If you include this file in your code repository, anyone that clones your repository can get up and running with a single command.

It's important to understand that Compose is a declarative tool - you simply define it and go. You don't always need to recreate everything from scratch. If you make a change, run docker compose up again and Compose will reconcile the changes in your file and apply them intelligently.

Dockerfile versus Compose file

A Dockerfile provides instructions to build a container image while a Compose file defines your running containers. Quite often, a Compose file references a Dockerfile to build an image to use for a particular service.

Try it out

In this hands-on, you will learn how to use a Docker Compose to run a multi-container application. You'll use a simple to-do list app built with Node.js and MySQL as a database server.

Start the application

Follow the instructions to run the to-do list app on your system.

  1. Download and install Docker Desktop.

  2. Open a terminal and clone this sample application.

    git clone https://github.com/dockersamples/todo-list-app 
    
  3. Navigate into the todo-list-app directory:

    cd todo-list-app
    

    Inside this directory, you'll find a file named compose.yaml. This YAML file is where all the magic happens! It defines all the services that make up your application, along with their configurations. Each service specifies its image, ports, volumes, networks, and any other settings necessary for its functionality. Take some time to explore the YAML file and familiarize yourself with its structure.

  4. Use the docker compose up command to start the application:

    docker compose up -d --build
    

    When you run this command, you should see an output like this:

    [+] Running 5/5
    ✔ app 3 layers [⣿⣿⣿]      0B/0B            Pulled          7.1s
      ✔ e6f4e57cc59e Download complete                          0.9s
      ✔ df998480d81d Download complete                          1.0s
      ✔ 31e174fedd23 Download complete                          2.5s
      ✔ 43c47a581c29 Download complete                          2.0s
    [+] Running 4/4
      ⠸ Network todo-list-app_default           Created         0.3s
      ⠸ Volume "todo-list-app_todo-mysql-data"  Created         0.3s
      ✔ Container todo-list-app-app-1           Started         0.3s
      ✔ Container todo-list-app-mysql-1         Started         0.3s
    

    A lot happened here! A couple of things to call out:

    • Two container images were downloaded from Docker Hub - node and MySQL
    • A network was created for your application
    • A volume was created to persist the database files between container restarts
    • Two containers were started with all of their necessary config

    If this feels overwhelming, don't worry! You'll get there!

  5. With everything now up and running, you can open http://localhost:3000 in your browser to see the site. Note that the application may take 10-15 seconds to fully start. If the page doesn't load right away, wait a moment and refresh. Feel free to add items to the list, check them off, and remove them.

    A screenshot of a webpage showing the todo-list application running on port 3000

  6. If you look at the Docker Desktop GUI, you can see the containers and dive deeper into their configuration.

    A screenshot of Docker Desktop dashboard showing the list of containers running todo-list app

Tear it down

Since this application was started using Docker Compose, it's easy to tear it all down when you're done.

  1. In the CLI, use the docker compose down command to remove everything:

    docker compose down
    

    You'll see output similar to the following:

    [+] Running 3/3
    ✔ Container todo-list-app-mysql-1  Removed        2.9s
    ✔ Container todo-list-app-app-1    Removed        0.1s
    ✔ Network todo-list-app_default    Removed        0.1s
    

    Volume persistence

    By default, volumes aren't automatically removed when you tear down a Compose stack. The idea is that you might want the data back if you start the stack again.

    If you do want to remove the volumes, add the --volumes flag when running the docker compose down command:

    docker compose down --volumes
    [+] Running 1/0
    ✔ Volume todo-list-app_todo-mysql-data  Removed
    
  2. Alternatively, you can use the Docker Desktop GUI to remove the containers by selecting the application stack and selecting the Delete button.

    A screenshot of the Docker Desktop GUI showing the containers view with an arrow pointing to the "Delete" button

    Using the GUI for Compose stacks

    Note that if you remove the containers for a Compose app in the GUI, it's removing only the containers. You'll have to manually remove the network and volumes if you want to do so.

In this walkthrough, you learned how to use Docker Compose to start and stop a multi-container application.

Additional resources

This page was a brief introduction to Compose. In the following resources, you can dive deeper into Compose and how to write Compose files.

Roll out Docker to a pilot group

Rolling out Docker across an organization changes how developers sign in and how Docker Desktop behaves. Testing the managed experience with a small group helps uncover access or workflow problems before they affect everyone.

In this 15-minute tutorial, you'll invite one pilot user, apply one managed setting to that user, and confirm that Docker Desktop receives the policy.

Before you start

You need:

Invite the pilot user

  1. Sign in to Docker Home and select your organization.
  2. Select Members, then Invite.
  3. Select Emails or usernames and enter the pilot user's email address.
  4. Assign the Member role and a Docker Desktop license, then send the invitation.
  5. Have the pilot user accept the invitation from the email or the Notifications Center.

The pilot user is now part of the organization and can receive its Docker Desktop policies.

Create a pilot policy

  1. In Docker Home, select Docker Desktop, then Settings Management.
  2. Select Create a settings policy.
  3. Name the policy Docker pilot.
  4. Apply the policy to Specific users, then select the pilot user.
  5. Set Automatically update components to Always enabled. Leave the other settings as User-defined.
  6. Select Create.

This policy locks one low-impact setting, making the result visible without changing the pilot user's development workflow.

Apply and verify the policy

Ask the pilot user to:

  1. Quit Docker Desktop completely.
  2. Start Docker Desktop and sign in with the account that joined the organization.
  3. Open Docker Desktop settings and confirm that Automatically update components is enabled and can't be changed.
  4. Run a familiar development project to confirm their normal workflow still works.

Docker Desktop checks for policy updates when it starts and every 60 minutes while running. Fully quitting and reopening it applies the policy without waiting for the next check.

What you learned

You added a user, targeted a policy, and verified the managed Docker Desktop experience before applying controls across the organization. Use the pilot's feedback to refine the policy and its deployment communication.

What's next

Continue with these guides when you're ready to expand the deployment:

Run your coding agent in a sandbox

Coding agents can work faster in full-autonomy modes that let them run commands, install tools, and use Docker without stopping for approval. Giving an agent that access directly on your machine also gives mistakes a wider reach.

Docker Sandboxes change where the agent runs, not how you work with it. The agent gets a private environment with its own operating system and Docker daemon. Your project remains available on your host, while tools the agent installs and system changes it makes stay inside an environment you can discard.

[!NOTE] Familiar with containers? See how Docker Sandboxes differ from containers.

In this tutorial, you'll move an existing coding-agent workflow into a local sandbox. For a cloud workflow, see Get started with cloud sandboxes.

Before you start

You don't need Docker Desktop or Docker Engine on your host.

Install and sign in

Install Docker Sandboxes for your operating system, then sign in to Docker:

$ sbx login

Bring your agent setup

This step is optional. Skip it if you want to start with a clean agent setup.

If you use agent skills on your host, preview and import them:

$ sbx skills import --dry-run
$ sbx skills import

Imported skills become available to supported agents across your sandboxes. This feature is experimental. See Share agent skills for supported agents and security considerations.

Choose how to authenticate

Your agent needs access to a model provider. Subscription sign-in uses OAuth, while API keys are stored on your host and supplied to the agent through the sandbox proxy. Choose your agent and authentication method for the preparation steps:

{{< sandbox-auth >}}

If you already exported a supported API key in your shell, you can import it instead of setting it separately:

$ sbx secret import --dry-run
$ sbx secret import

The dry run shows which exported keys sbx found. The import command prompts you to confirm each key before storing it. If the dry run finds nothing, use the sbx secret set command from the picker. See Credentials for other secret sources and supported coding agents for agent-specific authentication.

Run your agent

Open your project and start your preferred agent. This example uses Codex:

$ cd ~/my-project
$ sbx run --name my-project codex

Replace codex with another supported agent identifier, such as claude, copilot, cursor, or gemini.

The first time you run a sandbox, sbx asks you to choose a default network policy. This policy controls which external services your sandboxes can reach. Select Balanced to permit common development services and block other destinations by default. You can change these rules later with sbx policy.

The built-in integrations start coding agents in their full-autonomy mode. For example, Codex bypasses approvals, Claude Code skips permission prompts, and Cursor, Copilot, and Gemini use YOLO mode. You don't need to add those flags.

Give the agent the same task you would give it on your host. Source changes appear in your working tree, so you can inspect them with your usual tools:

$ git diff

Your project directory is the exception to the sandbox boundary. It is shared read-write, so the agent can modify or delete its files and you can see those changes immediately. Keep your work under version control. Tools the agent installs and changes to the sandbox's operating system stay inside the sandbox.

Return or start over

Exit the agent when you're finished. Reconnect to the same environment later:

$ sbx run --name my-project

When you want a clean environment, remove the sandbox:

$ sbx rm my-project

Removing the sandbox deletes the environment and everything installed inside it. It doesn't delete your project directory.

What changed

You kept your project, agent, skills, credentials, and prompting workflow. The only essential change was launching the agent with sbx run. That moved its full-autonomy execution into an environment you control and can throw away.

What's next

Continue with the Docker Sandboxes manuals:

Build and share a containerized application

An application can depend on a particular runtime, libraries, database, and supporting tools. Docker packages these dependencies with the application so you can run it consistently without preparing each machine by hand.

In this 15-minute tutorial, you'll run a container, start a multi-container application, package the application as an image, and share the image through Docker Hub.

Before you start

Run a container

Start with a small web application that someone else has packaged for Docker. Docker distributes applications in images. An image is a ready-to-run package that contains an application and everything it needs. A container is a running instance of an image.

Download the image from Docker Hub:

$ docker pull docker/welcome-to-docker

Start a container from the image:

$ docker run --detach --name welcome --publish 8080:80 docker/welcome-to-docker

This command runs the container in the background (--detach), names it welcome (--name), and makes its web server available at port 8080 on your machine (--publish 8080:80). The final argument identifies the image to run.

Open http://localhost:8080 to see the application. You downloaded an image and started one container from it.

Remove the container before continuing:

$ docker rm --force welcome

This removes the running application. The image remains available locally, so Docker can create another container from it later.

Run an application stack

The first application needed only one container. Applications often have several parts, such as a frontend, an API, and a database. You could start each part with a separate docker run command, but you would also need to keep their configuration and connections in sync.

Docker Compose describes all the parts of an application in a compose.yaml file and manages them together. Compose is included with Docker Desktop.

Try it with a prepared to-do application. Clone the project and open its directory:

$ git clone https://github.com/docker/getting-started-todo-app
$ cd getting-started-todo-app

The project's compose.yaml file defines five services: a frontend, an API, a database, a database management interface, and a proxy. A service represents one part of the application and runs in its own container.

Start the complete development stack:

$ docker compose up --build --detach

The docker compose up command reads compose.yaml and starts its services. The --build option builds the frontend and API images from the project, and --detach leaves the containers running in the background. Compose also pulls the images for the supporting services and connects the containers.

Open http://localhost, then add an item to the to-do list.

See the containers that Compose started:

$ docker compose ps

Each row represents a container for one of the application's services. Instead of preserving a collection of commands, the project keeps the complete stack in one version-controlled definition.

Build an image

So far, you have run a published image and used Compose to build and manage a development stack. Next, build an image of your own that packages the to-do application's frontend and API together.

The repository contains a Dockerfile, which is a set of instructions for building the image. Replace <YOUR_DOCKER_USERNAME> with your Docker username, then run:

$ docker build --tag <YOUR_DOCKER_USERNAME>/getting-started-todo-app .

The --tag option gives the image a name. The username prefix identifies where the image will be stored on Docker Hub. The final . tells Docker to find the Dockerfile and application source in the current directory.

Create a container from your image, as you did with the welcome image:

$ docker run --detach --name todo --publish 8080:3000 <YOUR_DOCKER_USERNAME>/getting-started-todo-app

Open http://localhost:8080. This time, the frontend and API are running together from the image you built.

Share the image

Images can be shared through a registry. Docker Hub is a registry for storing images and making them available to other machines and deployment systems.

In Docker Home, create a public repository named getting-started-todo-app under your Docker username.

Sign in from the command line, then push the image:

$ docker login
$ docker push <YOUR_DOCKER_USERNAME>/getting-started-todo-app

The docker push command uploads the image to Docker Hub. Open the repository in Docker Home to see it. Another machine or deployment system can pull and run the same image.

Clean up

Remove the standalone container and the development stack:

$ docker rm --force todo
$ docker compose down --volumes

What you learned

You ran software packaged by someone else, started a multi-container application with Compose, built your own image, and published it. An image packages an application, a container runs that image, Compose coordinates multiple containers, and a registry makes images available beyond one machine.

What's next

Explore the concepts from this tutorial in more detail:

Choose a language-specific Docker guide to containerize an application of your own.