A Beginner's Guide to Docker

REFERENCE
12 min read

Recently we announced that the latest version of DoltLab Enterprise uses Docker Swarm for its multihost deployment orchestration and we thought this was a good opportunity to write a beginner-friendly blog series about Docker.

Part one of the series, today's post, will cover the most basic version of Docker, the standalone, command line tool. The next post in our series will introduce Docker Compose, which enables easy configuration of multiple Docker containers using a single configuration file. And finally, we'll cap off the series with a beginner friendly guide to Docker Swarm, a mode that allows for replication and fail over of Docker containers across hosts, which is all orchestration by the Docker engine!

Let's jump right into basic Docker.

Docker

Docker is a platform that packages applications into standardized units called "containers". Each container contains everything that an application needs to run: code, binaries, a runtime environment, system tools, libraries, and settings.

In fact, you can think of containers as light-weight virtual machines. The biggest benefit of running applications in this way, often said to be "containerized applications", is that containers perfectly encapsulate an application runtime environment that can be built and run in a repeatable way. This is possible since containers are built from user-defined Docker images, or build instructions, that consist of a series of build steps which Docker will execute in order, during the build process. Once an image is built, a runnable instance of that built image, called a "container", can be launched.

Additionally, Docker images can be easily distributed, shared, and downloaded, often for free, which is why its such a popular and convenient option for running or deploying applications. Typically Docker images are hosted on remote "container registries" like DockerHub, where you can find existing images for use.

Let's look at an example of using standalone Docker locally, to get a better understanding of how these "images" and "containers" enable quick, consistent application runtimes.

Before beginning, you'll need to have the latest version of Docker installed.

Next, let's create a simple Docker image. This is the starting point for any "dockerized" application. For this example, we'll create an image that will have the curl utility installed and ready to use. For this, we'll use the latest Ubuntu runtime environment as our base environment, then we'll install curl on it. Though very simplistic, we'll pretend that this is the "application" we want to run. Don't be fooled, though, this basic process what you follow to create any Docker image!

To make our image, we can define a Dockerfile that contains the following:

FROM ubuntu:latest
RUN apt update -y && apt install curl -y

A Dockerfile is how you define how an image should be built. Docker will execute each step of the Dockerfile in order, and when the build process is complete, any container created from the resulting image will start running with the final state achieved at the end of the build process. For us, we want to install curl during the build process, so that when we run a container, curl will be installed and ready to use when the container launches.

In our simple example Dockerfile above we lay out our build steps to:

  • First, use the latest Ubuntu Docker image as our base image, or starting point. This is specified in a Dockerfile by using the FROM keyword.
  • Then, we want Docker to execute the two apt commands that will install curl on in our container runtime. These commands are specified with the RUN keyword

which lets you execute arbitrary commands during Docker's image build process.

If we save this file, we can build our image using the Docker CLI and the following arguments:

➜ ✗ docker build -f Dockerfile -t our-curl-app:latest .

We run the docker build command and specify our image file with -f, and then also name and tag our image with -t. We'll call our image our-curl-app and give it the latest tag.

Docker will output information about its execution during the build process that looks like the following output. Notice you can see it executes each step in our Dockerfile.

[+] Building 12.5s (6/6) FINISHED                                                                                 docker:default
 => [internal] load build definition from Dockerfile                                                                        0.0s
 => => transferring dockerfile: 98B                                                                                         0.0s
 => [internal] load metadata for docker.io/library/ubuntu:latest                                                            1.3s
 => [internal] load .dockerignore                                                                                           0.0s
 => => transferring context: 2B                                                                                             0.0s
 => [1/2] FROM docker.io/library/ubuntu:latest@sha256:a08e551cb33850e4740772b38217fc1796a66da2506d312abe51acda354ff061      3.0s
 => => resolve docker.io/library/ubuntu:latest@sha256:a08e551cb33850e4740772b38217fc1796a66da2506d312abe51acda354ff061      0.0s
 => => sha256:a08e551cb33850e4740772b38217fc1796a66da2506d312abe51acda354ff061 6.69kB / 6.69kB                              0.0s
 => => sha256:4f1db91d9560cf107b5832c0761364ec64f46777aa4ec637cca3008f287c975e 424B / 424B                                  0.0s
 => => sha256:65ae7a6f3544bd2d2b6d19b13bfc64752d776bc92c510f874188bfd404d205a3 2.30kB / 2.30kB                              0.0s
 => => sha256:32f112e3802cadcab3543160f4d2aa607b3cc1c62140d57b4f5441384f40e927 29.72MB / 29.72MB                            2.3s
 => => extracting sha256:32f112e3802cadcab3543160f4d2aa607b3cc1c62140d57b4f5441384f40e927                                   0.7s
 => [2/2] RUN apt update -y && apt install curl -y                                                                          7.2s
 => exporting to image                                                                                                      0.9s
 => => exporting layers                                                                                                     0.9s
 => => writing image sha256:1a4e7476dcf2edd499567b74fae46921b3f9d0e077ff4897ff49185293f1c447                                0.0s
 => => naming to docker.io/library/our-curl-app:latest                                                                      0.0s

Once our image is built, we can see it locally available in Docker using the docker image list command:

➜ ✗ docker image ls
REPOSITORY     TAG       IMAGE ID       CREATED         SIZE
our-curl-app   latest    1a4e7476dcf2   4 minutes ago   139MB
➜ ✗

Now, we can use the image to create a "container" of our curl application by running our image with the docker run command.

This creates a single, runnable instance of our image. And when the container is started, curl will already be installed, ready to use, since the step of installing curl was performed at build time.

➜ ✗ docker run -it our-curl-app:latest /bin/bash
root@2e9dcbf7a26d:/#

The -it flag tells Docker we want to open an interactive terminal when we run the container. We then specify the image we want the container to be created from, which is our-curl-app:latest, and finally, we provide the shell to use for our interactive terminal, /bin/bash, which comes pre-installed in the ubuntu:latest base image.

Once we get a shell into our running container, we can start using curl! Let's hit Google.

root@2e9dcbf7a26d:/# curl -I https://www.google.com
HTTP/2 200
content-type: text/html; charset=ISO-8859-1
content-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce-DqDXc61FjMu9KcQ9_0y6Nw' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp
accept-ch: Sec-CH-Prefers-Color-Scheme
p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
date: Tue, 22 Jul 2025 18:23:17 GMT
server: gws
x-xss-protection: 0
x-frame-options: SAMEORIGIN
expires: Tue, 22 Jul 2025 18:23:17 GMT
cache-control: private
set-cookie: AEC=AVh_V2i025GrmBoDgehkSzfpCOA-_mdWthbAfGuwrPk0pvOa_NjGksxpLL8; expires=Sun, 18-Jan-2026 18:23:17 GMT; path=/; domain=.google.com; Secure; HttpOnly; SameSite=lax
set-cookie: NID=525=LNt8Yslm1fmpPI3Jkdedsth8bWnUp0EFX3VuaY2kM6CSZl8fUlyOuv-okiU-9gyiQxsg22DeKvdeKpCnlkef9nW0tDW7w-ND0lljWPDdqjOSLEF1l-xlgbVdssIO_MPjJPWluop55MOksZZJ6LMDNeI6S4F1LdemztESCzKGBNsCXo6vwrmAr7D-_pOhe2wFHV1N6VCzP1Nak1J6Zf4; expires=Wed, 21-Jan-2026 18:23:17 GMT; path=/; domain=.google.com; HttpOnly
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

root@2e9dcbf7a26d:/#

After we exit our interactive shell, our container will stop, but we can see all available containers using the docker container list command:

➜ ✗ docker container ls -a
CONTAINER ID   IMAGE                 COMMAND       CREATED         STATUS                      PORTS     NAMES
2e9dcbf7a26d   our-curl-app:latest   "/bin/bash"   5 minutes ago   Exited (0) 35 seconds ago             youthful_brown

By default, the container list command will only display running containers, but we can use the -a flag to view all containers that exist locally.

If we ever want to rerun our container, we can simply use the docker run command with the container ID 2e9dcbf7a26d, and Docker will start our container back up!

But, what's really cool about Docker is, as I mentioned earlier, you don't necessarily need to create your own images if one that meets your needs already exists and is free to use.

Let's extend our standalone example a bit further by using an existing Docker image that provides us access to curl so we don't need to write a custom Dockerfile.

To do this, we can simply pull an existing curl image from DockerHub, the official container registry of Docker, and run it locally:

➜ ✗ docker run curlimages/curl:latest -I https://www.google.com

We could have used the docker pull command first to pull the image, but actually, the Docker CLI will automatically attempt to pull an image it cannot find locally. So executing the above command will output the following:

Unable to find image 'curlimages/curl:latest' locally
latest: Pulling from curlimages/curl
9824c27679d3: Pull complete
cb1ae706b42e: Pull complete
bd9ddc54bea9: Pull complete
Digest: sha256:4026b29997dc7c823b51c164b71e2b51e0fd95cce4601f78202c513d97da2922
Status: Downloaded newer image for curlimages/curl:latest
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
HTTP/2 200
content-type: text/html; charset=ISO-8859-1
content-security-policy-report-only: object-src 'none';base-uri 'self';script-src 'nonce-Yo4oQ29qi9GAtY1Cm1AbAQ' 'strict-dynamic' 'report-sample' 'unsafe-eval' 'unsafe-inline' https: http:;report-uri https://csp.withgoogle.com/csp/gws/other-hp
accept-ch: Sec-CH-Prefers-Color-Scheme
p3p: CP="This is not a P3P policy! See g.co/p3phelp for more info."
date: Tue, 22 Jul 2025 18:36:03 GMT
server: gws
x-xss-protection: 0
x-frame-options: SAMEORIGIN
expires: Tue, 22 Jul 2025 18:36:03 GMT
cache-control: private
set-cookie: AEC=AVh_V2gm5Scb_Wjc6EHkQMZWrvpMqyZc2FuvbN2HRYwXWR_n_Y6FaziDcA; expires=Sun, 18-Jan-2026 18:36:03 GMT; path=/; domain=.google.com; Secure; HttpOnly; SameSite=lax
set-cookie: NID=525=X7TkqEpVJCJB5hap838roA8lORAW6YAH9LKZ_3NFtwy54pj2yp6n1_4HWP72CO9YJc0VhL0Qnpj9aSZcLokoaDdByFGX1BDmX__Wj06xsGoBYBtvG_qFQVhf8FkZslD6o8GodzelREkBGD7fKCT7SLijWmQ5a_5tX6hRHwuiumGe-dcLSuYx04rx5JY_RGNx0q9va2EVlkfxV1dmcpw; expires=Wed, 21-Jan-2026 18:36:03 GMT; path=/; domain=.google.com; HttpOnly
alt-svc: h3=":443"; ma=2592000,h3-29=":443"; ma=2592000

We can see that Docker pulled the latest curlimages/curl image, and then passed the arguments we specified, -I https://www.google.com, directly to the running curlimages/curl container, which when running, just runs the curl binary.

This is why we see the same output we saw before, only now, we didn't need an interactive shell into the running container in order to execute curl.

Pretty cool right!

One of the nicest parts of executing containerized applications in this fashion is that you don't need to install the application you want to use locally anymore. You only need to have Docker installed, and then you can run any containerized application that has a Docker image. This makes testing and deploying applications so easy and simple, which is why Docker is so popular.

Let's do one more example where we run a standalone Docker container, but this time, let's run a web server application and serve it on port 80.

We'll run an nginx server that serves the text "hello world!" on port 80.

Without Docker, we'd need to install nginx locally, but with Docker, we don't need to! We just need to run an nginx image and provide it an index.html file to serve!

First, create index.html that contains the following:

<!DOCTYPE html>
<html>
<head>
    <title>Hello World</title>
</head>
<body>
    <h1>Hello, World from Dockerized NGINX!</h1>
</body>
</html>

Then, use the following docker run command:

➜ ✗ docker run -v "$(pwd)"/index.html:/usr/share/nginx/html/index.html -p 80:80 -d nginx:alpine

The command above mounts the local index.html file we created to the path /usr/share/nginx/html/index.html inside the nginx container where it will be served from by using the -v flag for volume mounts.

The -p flag maps our local port 80 to the running container's port 80 so that localhost:80 will forward to the container on port 80. And finally, the -d flag tells Docker to run this container in daemon mode.

The output of this command is the following:

Unable to find image 'nginx:alpine' locally
alpine: Pulling from library/nginx
9824c27679d3: Already exists
a5585638209e: Pull complete
fd372c3c84a2: Pull complete
958a74d6a238: Pull complete
c1d2dc189e38: Pull complete
828fa206d77b: Pull complete
bdaad27fd04a: Pull complete
f23865b38cc6: Pull complete
Digest: sha256:d67ea0d64d518b1bb04acde3b00f722ac3e9764b3209a9b0a98924ba35e4b779
Status: Downloaded newer image for nginx:alpine
190a308f114a367418e7a376f5839b9b1a3ba21442b2fc4a6705297d1084d3da

Similar to before we see that Docker pulls the nginx:alpine image automatically since it did not find the image locally. Then it starts the container in the background.

If we run docker ps we can see the container process:

➜ ✗ docker ps
CONTAINER ID   IMAGE          COMMAND                  CREATED         STATUS         PORTS                                 NAMES
190a308f114a   nginx:alpine   "/docker-entrypoint.…"   8 minutes ago   Up 8 minutes   0.0.0.0:80->80/tcp, [::]:80->80/tcp   focused_volhard

Great! Now let's see what happens if we curl our local port 80:

➜ ✗ curl http://localhost:80
<!DOCTYPE html>
<html>
<head>
    <title>Hello World</title>
</head>
<body>
    <h1>Hello, World from Dockerized NGINX!</h1>
</body>
</html>

Boom, We get our "hello world" page! And, Docker will continue running our simple web application until we tell it to stop. We can do so any time by using the docker stop command:

➜ ✗ docker stop 190a308f114a
190a308f114a

We can confirm Docker stopped the process by running docker ps again.

➜ ✗ docker ps
CONTAINER ID   IMAGE     COMMAND   CREATED   STATUS    PORTS     NAMES

Conclusion

Now that you've been exposed to the basic version of Docker and some of its command line options, we encourage you to explore more on your own.

There is so much more you can do with basic Docker and the above is just the tip of the iceberg.

Don't forget to keep an eye out for the next blog in this series which will introduce you to Docker Compose, which will lay the ground work for our final post in the series which will get you started with Docker Swarm.

See you then!

SHARE

JOIN THE DATA EVOLUTION

Get started with Dolt

Or join our mailing list to get product updates.