Docker on macOS: Getting Started

In this Docker tutorial, you’ll learn Docker vocabulary and the commands for creating, inspecting and removing containers, networks and data volumes. You’ll learn how to run Docker containers in the background or foreground, and switch between the two; how to publish ports; how to connect a database app and a web app running in separate containers; and how to share directories between containers and your Mac and among containers.

Version

  • Other, Other, Other

Docker is a tool that makes it easy to run applications in containers. Containers provide isolation and security like virtual machines, but they’re much smaller because they run in the host machine’s system.

As an iOS developer, why would you want to use Docker? To avoid version problems — to run versions of operating systems, programming languages, database apps, web apps and web servers, machine learning programs — all in isolated environments, to avoid side effects from whatever else you’ve installed on your Mac. Basically, to join the ranks of it-works-on-my-machine developers!

In this tutorial, you’ll get comfortable with Docker vocabulary and commands for creating, inspecting and removing containers, networks and data volumes. You’ll learn how to run Docker containers in the background or foreground, and switch between the two; how to publish ports; how to connect a database app and a web app running in separate containers; and how to share directories between containers and your Mac, and among containers. After completing this tutorial, you’ll be well on your way to Docker ninjadom!

Note: This Docker tutorial assumes you’re willing to enter Unix commands in Terminal. Some familiarity with Unix commands and file paths, database apps and localhost ports will be helpful.

Getting Started

Click the Download Materials button at the top or bottom of this tutorial to get the project files you’ll use to build the web server app, later in this tutorial.

Installing Docker

Docker was originally developed for Linux. Running Docker on macOS used to be pretty complicated, but a native macOS app Docker for Mac launched in July 2016, so now it’s a breeze!

The Community Edition (CE) is a free download, so download Docker CE for Mac, then install and run the app. Moby the whale should appear in your Mac’s status bar:

Note: If the direct download link above doesn’t work, go to Docker’s web page, click the Please Login to Download button, create an account, then wait for the activation email. While you wait, use the online Play-With-Docker playground.

Docker Terminology & Housekeeping

In this section, you’ll learn some Docker vocabulary and get comfortable with using the Unix command line for basic housekeeping tasks.

Note: There’s a handy list of all the commands in this tutorial — scroll down to the end.

Hello World

Open Terminal, and enter this command to see that Docker is running OK:

docker run hello-world

The workhorse Docker command is docker run, and this is the simplest docker run command — it specifies the Docker image to run. If the image is not on your host system, it tries to pull it from the default Docker image registry. The meaning of the word image is similar to the disk images you download as .dmg files. A Docker image is an app, and you run it on your system in a Docker container.

The output of this command explains what Docker just did:

Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
d1725b59e92d: Pull complete 
Digest: sha256:0add3ace90ecb4adbf7777e9aacf18357296e799f81cabc9fde470971e499788
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

Wow, lots of new terms to learn! Starting from the top:

  • Although you only asked for hello-world, Docker looked for hello-world:latest — latest is a tag indicating the version. If you don’t ask for a specific version, Docker assumes you want the latest version.
  • Because the image isn’t on your system, Docker pulls it from library/hello-world — its location in the default image registry Docker Hub. You’ll visit this place in the Running a Web App section.
  • The Docker client and Docker daemon are parts of the Docker Engine, which is the client-server application now running on your Mac. The daemon is the server, and the client is the docker command line interface (CLI). The client gives instructions to the daemon using the engine’s REST API.

Docker engine (image from docs.docker.com)

Note: You’ll learn about Docker’s network and data volume features later in this tutorial.

There are many types of Docker images:

  • OS: mostly Linux flavors like Ubuntu, Alpine etc.
  • programming languages: Swift, Ruby, PHP, Haskell, Python, Java, Golang etc.
  • databases: MySQL, CouchDB, PostgreSQL, Oracle, IBM Db2 etc.
  • application frameworks: Node.js, Flask, Kitura, Tomcat etc.
  • web servers: Nginx, Apache etc.
  • web apps including Python apps and Jupyter notebooks for machine learning

A Docker image consists of layers — lower layers (OS or programming language) are used by higher layers (API or app). Running an image creates a container — a thin read-write layer on top of the read-only layers of the image. You can run the same image in multiple containers using very little memory. Each container is just the read-write layer, and only one copy of the image exists on your system. The image’s top read-only layer specifies the command to run in the container — for hello-world, this command just outputs the Hello from Docker! message.

Container layer on top of image layers (image from docs.docker.com)

Using Docker Commands

Now to the housekeeping part — you need to keep track of what Docker is creating on your system, so you can remove stuff when you don’t need it anymore.

First, some general information about the Docker command line interface (CLI) syntax: Docker commands are similar to Unix commands, but they start with “docker”, like docker run, docker image, docker container, docker network.

Most commands have several options, and many options have shorthand versions. The full name of the option is --something, with two dashes, like --name or --publish. The shorthand version is -abbrev, with one dash, like -p for --publish or -v for --volume. A few options, like --name, don’t have a shorthand version.

Most options require values, like -p 8080:8080 or --name kitura. A few don’t have values, and can be run together, like -it or -ti, short for --interactive --tty. You must specify an option’s value right after the option name, but options and option-value pairs can appear in any order.

Many option values map something on the host machine to something in the container. In your case, the host machine is your Mac.

You’ll use all of these, and more, in this tutorial.

Start by entering this command in the terminal window:

docker images

The output lists the Docker images on your system, in particular the hello-world:latest image that Docker pulled:

REPOSITORY    TAG     IMAGE ID      CREATED       SIZE
hello-world   latest  e38bc07ac18e  2 months ago  1.85kB
Note: Your output will show different IMAGE ID and CREATED values.

As the output of docker run hello-world says, the Docker daemon created a container from this image, to run the executable that produced the output.

Run this command to show all (-a) the Docker containers on your system:

docker ps -a
Note: This does the same thing as docker container ls -a. For both commands, leaving off the -a option lists only containers that are running.

The output shows just one container:

CONTAINER ID  IMAGE        COMMAND   CREATED    STATUS  PORTS  NAMES
4ed31ad50912  hello-world  "/hello"  16 sec...  Exited ...     stupefied_gates

Docker created an ID and a name for the container — your values will be different. Although the container exited, it’s still on your system.

Run hello-world, then show all containers again:

docker run hello-world
docker ps -a
Note: Use the Up Arrow and Down Arrow keys to navigate through the Unix commands you’ve run in this terminal window.

Now there’s a second container, with different ID and name values:

CONTAINER ID  IMAGE        ...  NAMES
4ed31ad50912  hello-world  ...  stupefied_gates
e5d3669f5ca1  hello-world  ...  flamboyant_zhukovsky

You can remove a container by specifying its name or its ID or just the first 3 characters of its ID, and you can remove several containers in a single command, like so (your ID and name will be different):

docker rm e5d stupefied_gates

The output just echoes the container IDs or names. Confirm the containers are gone:

docker ps -a

It’s fun to see the names that Docker comes up with but, when you’ll be working with a container for a while, it’s convenient to give your own name to the container. Enter this command:

docker run --name helloWorld hello-world

Then list the container:

docker ps -a

And you’ve named your container:

CONTAINER ID  IMAGE        ...  NAMES
c5f411a593a3  hello-world  ...  helloWorld

Run the same command again:

docker run --name helloWorld hello-world

Now you get an error message, because that container name is already in use:

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

Next, I want to show you a cool housekeeping trick, so run docker run hello-world several times, to get lots of exited containers cluttering up your system. Then run this command to list them:

docker ps -a -q -f status=exited

This is the docker ps -a command you’ve been using to show all the Docker containers on your system, plus two options. Option -q is short for --quiet, so the command displays only the numeric IDs. Option -f is short for --filter, and the filter condition is status=exited. So the output looks like this (your IDs will be different):

d8d962602abf
64b2eb1af5da
5fde263a26a0
1659b24f2ce2
ff5e7f6a17b5
ab4bf3b4c32b
0d0e48dfcf32
18d8beb2fe60
c5f411a593a3 

Now, instead of copy-pasting each ID into the docker rm command, just feed this output into the docker rm command:

docker rm $(docker ps -a -q -f status=exited)

This command first runs the part inside $(), to get the list of IDs for exited containers, then removes them all — brilliant!

Now that you’ve removed the containers, you can also remove the image:

docker rmi hello-world

Your output looks similar to this:

Untagged: hello-world:latest
Untagged: hello-world@sha256:f5233545e43561214ca4891fd1157e1c3c563316ed8e237750d
59bde73361e77
Deleted: sha256:e38bc07ac18ee64e6d59cf2eafcdddf9cec2364dfe129fe0af75f1b0194e0c96
Deleted: sha256:2b8cbd0846c5aeaa7265323e7cf085779eaf244ccbdd982c4931aef9be0d2faf

Check for images again:

docker images

And your system is now clean:

REPOSITORY          TAG                 IMAGE ID            CREATED             SIZE
Time saver Tip — Docker cleanup terminal window: Move this terminal window to a corner of your desktop, and use it only for running Docker cleanup commands. This way, the command you want will be only a few Up Arrow keystrokes away. Another trick is to enter the Unix command history to find the number of the command you want, then run that command again by entering !. For example, below are the last few commands in my history, so entering the command !18 will remove all exited containers.
15  docker run --name helloWorld hello-world
16  docker ps -a
17  docker ps -a -q -f status=exited
18  docker rm $(docker ps -a -q -f status=exited)
19  docker rmi hello-world
20  docker images
21  history

Running a Web App in a Docker Container

Many iOS apps communicate with a web server, which is also the back end of a web app. And you often want interactions with the iOS app to be reflected in the web app, and vice versa. You can run the web app locally without containers, but running it in containers can make it easier to test different configurations in isolation, or to test user types with different privileges.

To run a web app in a container, the first thing you need to learn is how to access the container’s ports in your browser’s localhost. The Docker term is publishing ports. In this section, you’ll learn about publishing ports, and also take a quick look at the default Docker image registry Docker Hub.

Publishing Ports

Actually, you don’t need artificial intelligence to figure out ports! The syntax is --publish followed by two port numbers, separated by a colon. The trick is remembering the first value is for the host here, and the second value is for the container there — you’re publishing a port here from there. For the first number — the host’s port — you can specify any number you like, but the easiest option is to use the same number as the container’s port. For the second number, you’re stuck with whatever the container exposes — you’ll get an error if you try to change it. The ability to publish the container’s port to different localhost ports makes it easy to run several web app containers at the same time.

Start with a really simple web app from IBM Kitura. In a browser, load the default registry for Docker images at Docker Hub. Enter kitura in the Search field, then select the first returned item:

Open a new terminal window — this will be your Docker run terminal window. Sometimes, you’ll run a process in the foreground in this window, so you won’t be able to run any housekeeping commands in it. That’s where your Docker cleanup window will come in handy!

Click the copy icon for the Docker Pull Command, and paste it in your Docker run terminal window:

docker pull ibmcom/kitura-ubuntu

The ibmcom/kitura-ubuntu image layers a simple web app onto the Kitura application framework, which is layered onto the swift-ubuntu image, which is the Swift programming language layered on top of the Ubuntu operating system. Ubuntu is a version of Linux.

The image’s Docker Hub page doesn’t give any specific running instructions, so just go ahead and try it:

docker run ibmcom/kitura-ubuntu
Note: You could’ve just entered the command docker run ibmcom/kitura-ubuntu, instead of the pull command, and it would’ve pulled the image from Docker Hub before running. But sometimes, you might want to just download an image, then run it later — maybe after setting up other components.

The container starts, with these messages:

[2018-10-02T21:53:57.690Z] [WARNING] [ConfigurationManager.swift:261 load(url:
  deserializerName:)] Unable to load data from URL 
  /Kitura-Starter/config/mappings.json
[2018-10-02T21:53:57.698Z] [INFO] [main.swift:28 Kitura_Starter] Server will be 
  started on 'http://localhost:8080'.
[2018-10-02T21:53:57.703Z] [INFO] [HTTPServer.swift:124 listen(on:)] Listening 
  on port 8080
Note: You can also see a container’s ports in the PORTS column of the docker ps output.

It tried but failed to load data from a JSON file, then started listening on localhost:8080. Notice you didn’t get the Unix shell prompt back — this process is running in the foreground, and hasn’t exited.

Open a browser at localhost:8080 — nope, “Can’t Connect to the Server”. What’s happening? The container exposes port 8080 in its own environment, but you must publish this port to a port on the host system, to see what’s there.

First, get rid of this try-out container: In your Docker cleanup window, run a command to find the container’s ID, stop the container, then remove it. Because there’s only one container running, you can use a command, with two levels of $() nesting, that stops and removes all running containers. Try to work this out on your own before you check the solution below.

[spoiler title=”Solution”]docker rm $(docker stop $(docker ps -q))

This command stops and removes all running containers. If you have running containers you want to keep running, use docker ps on its own to find the ID of the running container you want to stop and remove.[/spoiler]

Go back to your Docker run terminal window — the Unix shell prompt has returned. Enter this command:

docker run -p 80:8080 --name kitura -d ibmcom/kitura-ubuntu

The -p option, short for --publish, maps the really local localhost:80 to the container’s 8080 port. The --name option specifies the container name. The -d option, short for --detach, runs the container process in the background, so you get the Unix shell prompt back right away.

In a browser, open localhost:80 to see this welcome page:

Run a second container on a different port:

docker run -p 90:8080 --rm --name kitura2 ibmcom/kitura-ubuntu

In the browser, open localhost:90 to see the same welcome page.

This time, the container is running in the foreground — you didn’t use the -d option, so you didn’t get the Unix shell prompt back. Also, the --rm option means the container will be removed when you stop it.

In your Docker cleanup window, enter this command:

docker stop kitura2

It might take several seconds before it echoes kitura2, and the Unix shell prompt returns in your Docker run window.

Use docker ps -a to see that there are no Exited containers:

CONTAINER ID  IMAGE                .. STATUS       PORTS                  NAMES
f64f7eb69258  ibmcom/kitura-ubuntu .. Up 47 sec..  0.0.0.0:80->8080/tcp   kitura

The first container is still running in the background, so enter this command to stop it:

docker stop kitura 

When the Unix shell prompt returns, refresh the localhost:80 page — it won’t load, because you stopped the container process.

Use docker ps -a to check the first container still exists, then run this command to restart it:

docker start kitura 

Refresh the localhost:80 page — this time it works!

Clean up: Close the browser windows. Stop and remove the container. Note that you can’t remove a running container unless you use force.

Running a Web App With a Database

In this section, you’ll first run a database app in a container, accessed by a web app running on the host system. You’ll need to publish the database app’s port, so the web app can access it.

If you want to run the web app on multiple ports, it’s easier if it runs in a container, too. But you’ll have to do some extra work to give the web app access to the database app.

Running CouchDB in a Docker Container

Web apps usually store data in a database app, like PostgreSQL or CouchDB. Running the database app in Docker containers lets you avoid version problems or test different versions or user types.

In your Docker run window, enter this command to run CouchDB in a Docker container:

docker run --name couchdb -p 5984:5984 -d couchdb 

The CouchDB image always exposes port 5984. You publish this to a local host port number so the web server app can access it.

Note: To specify user and password information when you run CouchDB in a container, use the --env (-e) option — for example:
docker run -e COUCHDB_USER=admin -e COUCHDB_PASSWORD=password --name couchdb -p 5984:5984 -d couchdb

While you wait for CouchDB, build a web server app that uses it. Don’t worry — it’s just a couple of Unix commands!

Open another terminal window — this is where you’ll enter commands that must run in the EmojiJournalServer directory. In Finder, locate the EmojiJournalServer folder in the Download Materials, then run these commands in the terminal window:

cd <drag the EmojiJournalServer folder from Finder to the terminal window> 
 
swift build 
.build/debug/EmojiJournalServer
Note: You’ll get some deprecation warnings as the app builds. Ignore them.

First, you change to the EmojiJournalServer directory, which contains a Package.swift file. The second command uses this file to build the web server app, and the third command runs it. The output looks like this:

[2018-10-22T14:56:58.919+11:00] [INFO] [Application.swift:45 
  connectionProperties] Running on MacOS - using local database
[2018-10-22T14:56:58.932+11:00] [WARNING] [ConnectionProperties.swift:57 
  init(host:port:secured:username:password:)] Initializing a CouchDB connection 
  without a username or password.
[2018-10-22T14:56:59.018+11:00] [INFO] [Application.swift:94 createNewDatabase()] 
  Database does not exist - creating new database
[2018-10-22T14:56:59.112+11:00] [INFO] [EntryRoutes.swift:43 
  initializeEntryRoutes(app:)] Journal entry routes created
[Mon Oct 22 14:56:59 2018] com.ibm.diagnostics.healthcenter.loader INFO: Swift 
  Application Metrics
[2018-10-22T14:56:59.206+11:00] [INFO] [Metrics.swift:47 
  initializeMetrics(app:)] Initialized metrics.
[2018-10-22T14:56:59.208+11:00] [INFO] [WebClientRoutes.swift:43 
  initializeWebClientRoutes(app:)] Web client routes created
[2018-10-22T14:56:59.218+11:00] [INFO] [HTTPServer.swift:124 listen(on:)] 
  Listening on port 8080

The last line of the output says it’s “listening on port 8080”, so open localhost:8080 in a browser to see the Kitura welcome page.

The “Journal entry route” is client, so add /client to the location URL to see the Emoji Journal app:

Click the smiley face to open the emoji menu. Select an emoji, then click the big + sign. This adds your chosen emoji to the journal:

Note: EmojiJournalServer is the sample web server app from our free video course Server Side Swift with Kitura. I’ve edited it a little, so it works in a Docker container.

This web server app is running on your host system, not in a Docker container. To run another server alongside, you must either edit and rebuild the Xcode project to expose a different port, or run the server in a Docker container. This is a Docker tutorial, so of course, you’re going to do the second thing!

Press Control-C in the EmojiJournalServer terminal window, to stop the server.

Running CouchDB & Server in Docker Containers

The EmojiJournalServer folder also contains Dockerfile, Dockerfile-tools and runDocker.sh files.

Enter these commands in the EmojiJournalServer terminal window:

cat Dockerfile
cat runDocker.sh 

You’re just looking at Dockerfile and runDocker.sh. Dockerfile-tools looks similar to Dockerfile. These files contain instructions for building the Docker images needed to run EmojiJournalServer. The runDocker.sh shell script contains the commands to build these images and run the app.

Enter this command to run the shell script — be sure to type the period and space, before runDocker.sh:

. runDocker.sh

This takes a while — go get something to drink, rack up another hour for your activity ring … you’ll end up with something like this:

[2018-10-24T05:25:20.387Z] [INFO] [Application.swift:50 connectionProperties] 
  No cloud credentials - running in Docker
[2018-10-24T05:25:20.390Z] [WARNING] [ConnectionProperties.swift:57 
  init(host:port:secured:username:password:)] Initializing a CouchDB connection 
  without a username or password.
[2018-10-24T05:25:20.432Z] [ERROR] [ClientRequest.swift:344 end(close:)] 
  ClientRequest Error, Failed to invoke HTTP request. CURL Return 
  code=CURLcode(rawValue: 6)
[2018-10-24T05:25:20.432Z] [INFO] [Application.swift:98 createNewDatabase()] 
  Database does not exist - creating new database
[2018-10-24T05:25:20.445Z] [ERROR] [ClientRequest.swift:344 end(close:)] 
  ClientRequest Error, Failed to invoke HTTP request. CURL Return 
  code=CURLcode(rawValue: 6)
[2018-10-24T05:25:20.446Z] [ERROR] [Application.swift:104 createNewDatabase()] 
  Could not create new database: (Optional("Internal Error")) - journal 
  entry routes not created
[2018-10-24T05:25:20.451Z] [INFO] [HTTPServer.swift:124 listen(on:)] 
  Listening on port 8080

This output looks similar to what you got from the .build/debug/EmojiJournalServer command, but “Database does not exist” seems wrong — why doesn’t it see the database you already created? And it gets worse, with “Could not create new database” and “journal entry routes not created”!

The last command in runDocker.sh published 8080 to 8090, so refresh or open localhost:8090 in a browser: the Kitura welcome page is there. But localhost:8090/client isn’t:

Cannot GET /client.

What’s happened? Both containers are running on the default bridge network. In your Docker run terminal window, enter this command to inspect it:

docker network inspect bridge

The output lists information about the emojijournal and couchdb containers:

"Containers": {
    "029114b903dd55ad4cce2160bd0c956439e939a8f6cfb81bb9a5f400ef953727": {
        "Name": "emojijournal",
        "EndpointID": "32ed8052f7e598b9fd027f5a227d5b277b53d66237b44018648e79dc22125c8c",
        "MacAddress": "02:42:ac:11:00:03",
        "IPv4Address": "172.17.0.3/16",
        "IPv6Address": ""
    },
    "2aaecbcb834ae405f4a5046f5195c1b2026b4122d1d05401b87fc6442085acf0": {
        "Name": "couchdb",
        "EndpointID": "043c1ec8eb3521caad9ec8ebacea2bf0a876e1c21cbfe3144f04be63ed3d9986",
        "MacAddress": "02:42:ac:11:00:02",
        "IPv4Address": "172.17.0.2/16",
        "IPv6Address": ""
    }
}

The IP address of couchdb is 172.17.0.2 in my output but might be different on your Mac or the next time I run CouchDB. You could edit and rebuild the web app to access CouchDB’s 5984 port as 172.17.0.2, but the IP address will probably be different when you run it. Ideally, you want the web app to automatically discover CouchDB’s IP address, whatever it is this time, from the host name couchdb.

The trick is to create a user-defined bridge network, then connect both containers to this network. Then each container can use the other’s host name to access its ports. An additional benefit is that entities outside the network cannot access these ports.

In your Docker cleanup terminal window, enter this command to stop and remove both containers:

docker rm $(docker stop $(docker ps -q))

Back in the EmojiJournalServer terminal window, run the following commands:

docker network create emoji-net

You’ve created a Docker network named emoji-net.

docker run --network emoji-net -d --name couchdb couchdb

You’ve started a CouchDB container on this network. You don’t need to publish a local port for CouchDB — the web app will run on the same network, so it will be able to see the port that CouchDB exposes to the network.

docker run --network emoji-net --name emojijournal -it -p 8090:8080 -v $PWD:/root/project -w /root/project emojijournal-run sh -c .build-ubuntu/release/EmojiJournalServer

And now you’ve started an EmojiJournalServer container on this network. This command is the same as the last command in runDocker.sh, with the network option added. Examining the rest of this command:

  • --name emojijournal names the container
  • -it ... sh -c .build-ubuntu/release/EmojiJournalServer creates an interactive terminal, and executes a shell command to run EmojiJournalServer
  • -p 8090:8080 publishes the container’s 8080 port to localhost:8090
  • -v $PWD:/root/project mounts the current directory in the container as /root/project — it bind your current directory to /root/project inside the container, so changes to the host files affect the container’s files and vice versa. Mounting Volumes has its own section later in this tutorial.
  • -w /root/project sets the container’s working directory to /root/project. -w is short for --workdir.

This time, it successfully creates a new database and journal entry routes:

...
[2018-10-25T22:53:32.642Z] [INFO] [Application.swift:98 createNewDatabase()] 
  Database does not exist - creating new database
[2018-10-25T22:53:32.702Z] [INFO] [EntryRoutes.swift:43 
  initializeEntryRoutes(app:)] Journal entry routes created
...
Note: The instructions for some Docker Hub images tell you to use --link to connect containers. Docker documentation says it’s “a legacy feature [that] may eventually be removed”, and you should use user-defined networks and --network.

The server is running in the foreground — press Control-P-Q to detach this process without stopping it. The Unix shell prompt returns.

Enter this command to inspect your network:

docker network inspect emoji-net

Scroll down to see both emojijournal and couchdb are listed in “Containers”:

"Containers": {
    "5fb6ef216c90489fec958c860915760f8d9b801800c6d0f8a70c9c8304c128d5": {
        "Name": "emojijournal",
        "EndpointID": "95871482d549441b22de602e5d27de21912cb85977c1ba4f5b74f5cb2fdc6fb6",
        "MacAddress": "02:42:ac:14:00:03",
        "IPv4Address": "172.20.0.3/16",
        "IPv6Address": ""
    },
    "686f1e8b59f1d95d482991bbc38e85d14bf1fd22bc7b87148f65fc899e834f2a": {
        "Name": "couchdb",
        "EndpointID": "1d7c116dae6d9039329de2db788cb27429330519257c24a5ec4fcac0764ab426",
        "MacAddress": "02:42:ac:14:00:02",
        "IPv4Address": "172.20.0.2/16",
        "IPv6Address": ""
    }
Note: The CouchDB container must be named “couchdb”, because this is what EmojiJournalServer expects. It’s hard-coded in Application.swift as the host name in the connectionProperties property of the App class.

In your browser, refresh or open localhost:8090/client and add an emoji or three. Then enter this command in the EmojiJournalServer terminal window — note the name is emojijournal2 and the published port number is 8070:

docker run --network emoji-net --name emojijournal2 -it -p 8070:8080 -v $PWD:/root/project -w /root/project emojijournal-run sh -c .build-ubuntu/release/EmojiJournalServer

This new emojijournal2 container finds the CouchDB database created by the first EmojiJournalServer:

[2018-10-25T23:38:14.632Z] [INFO] [Application.swift:80 postInit()] Journal 
  entries database located - loading...

Open localhost:8070/client in a browser — it displays the same emoji you added on port 8090. Add another emoji on port 8070, then refresh port 8090 to see it appear.

Where’s My Database?

Think back to the CouchDB container you ran on the default bridge network — you added emoji to it. Then you stopped and removed it, and later created another CouchDB container on the emoji-net network. Starting EmojiJournalServer on this network created a new empty journal entries database. Then starting a second EmojiJournalServer on the same network used the existing database.

The CouchDB container stores its databases in /opt/couchdb/data. The CouchDB container is running in the background, but docker exec lets you open an interactive terminal session, so you can take a look. Run this command in your Docker run terminal window:

docker exec -it couchdb bash

This command lets you peek at a container’s file system: docker exec runs its options on the couchdb container that you started in the background with the --detach option. Here, the docker exec options are --interactive and --tty, shortened to -it — these create an interactive terminal session in the CouchDB container. You specify bash as the command to run — bash is a specific Unix shell that lets you run Unix commands in the container. You get a shell prompt that ends with /opt/couchdb# — you’re already in the /opt/couchdb directory. At this prompt, enter this command:

ls -l data

If you run another CouchDB container, it has no access to this CouchDB container’s databases. Removing a CouchDB container also removes its databases. But you usually want to keep the data in your databases, between stopping and starting CouchDB containers. And you might want to share one container’s databases with another container.

As you’d expect, Docker has ways to let you do this!

Clean up: In the Docker run terminal window, enter exit at the bash prompt. Stop and remove all the containers.

Mounting Volumes: Sharing Host & Container Files

You’ll often want to share files or directories between your host machine and containers. Data in databases shouldn’t disappear when you stop and remove a container. It’s also convenient to share databases between different versions of apps, or different containers running the same database app. You might want to supply configuration files to a database app or training data to a machine learning app, when they’re running in containers. You’ll learn how to do these things in this section.

Persisting Databases After the Container Exits

To store the database on your local host system, so it persists and can be accessed by other containers, you’ll create a volume in Docker’s storage directory when you start the CouchDB container.

In the Docker run terminal window, enter this command:

docker run --mount source=couchdbVolume,target=/opt/couchdb/data --network emoji-net -d --name couchdb couchdb
Note: In the --mount value, don’t put spaces around the = signs or after the comma.

The first time you run this command, it creates couchdbVolume in Docker’s storage directory on your host system, then copies the contents of the container’s /opt/couchdb/data directory into couchdbVolume. The container then mounts and uses couchdbVolume to store its databases.

In your EmojiJournalServer terminal window, start an EmojiJournalServer container on the same network:

docker run --network emoji-net --name emojijournal -it -p 8090:8080 -v $PWD:/root/project -w /root/project emojijournal-run sh -c .build-ubuntu/release/EmojiJournalServer

Refresh or open localhost:8090/client, add emoji, then — in your Docker cleanup terminal window — stop and remove both containers:

docker rm $(docker stop $(docker ps -q))

The volume remains, even though you’ve removed the CouchDB container that created it. To see this, enter this command:

docker volume inspect couchdbVolume

The output looks like this:

[
  {
    "CreatedAt": "2018-10-26T08:00:59Z",
    "Driver": "local",
    "Labels": null,
    "Mountpoint": "/var/lib/docker/volumes/couchdbVolume/_data",
    "Name": "couchdbVolume",
    "Options": null,
    "Scope": "local"
  }
]

Use the Finder ▸ Go ▸ Go to Folder… menu item to look at the mountpoint directory … well, you can try to look at it — it’s not there! Docker’s storage directory actually lives in, and is managed by, the Docker engine. You’re not meant to access it directly on the host file system. You can only access it by mounting it in a Docker container.

Now, to demonstrate that the emoji database persisted after you removed the CouchDB container: Rerun the CouchDB command in your Docker run terminal window, and the EmojiJournalServer command in your EmojiJournalServer terminal window, then refresh localhost:8090/client — this time, you didn’t lose your emoji!

Because couchdbVolume already exists, the CouchDB container just mounted it, exposing the databases already stored there to the EmojiJournalServer container, which found and loaded the journal entries database created by the previous EmojiJournalServer container.

Clean up: Stop and remove the containers. Close the browser windows. Remove all unused networks:
docker network prune

Sharing a Database With Another Docker Container

You created a volume for CouchDB running on the emoji-net network. You’ve stopped and removed that CouchDB container, but the volume remains. You can reuse this volume for a CouchDB container running on the default network, effectively sharing its databases with another container.

To demonstrate this, mount couchdbVolume in a CouchDB container running on the default bridge network, so EmojiJournalServer on the host can access the saved journal entries database.

Start a CouchDB container on the default bridge network, mounting couchdbVolume, and publishing its port:

docker run --mount source=couchdbVolume,target=/opt/couchdb/data -p 5984:5984 -d --name couchdb couchdb

In your EmojiJournalServer terminal window, run EmojiJournalServer on your host system:

.build/debug/EmojiJournalServer

EmojiJournalServer, running on your host system, finds and loads the journal entries database in couchdbVolume, so when you load localhost:8080/client, you see the emoji from the EmojiJournalServer container:

In your EmojiJournalServer terminal window, press Control-C to stop this EmojiJournalServer. Close the browser window.

Stop and remove the CouchDB container, then enter this command to remove all unused volumes:

docker volume prune

Providing Local Files to a Docker Container

The command that runs EmojiJournalServer in a container has a -v $PWD:/root/project option that mounts the EmojiJournalServer directory as /root/project in the container. This section demonstrates that changes to the host files affect the container’s files, and vice versa.

In your EmojiJournalServer terminal window, start an EmojiJournalServer container in the background:

docker run --name emojijournal -itd -p 8090:8080 -v $PWD:/root/project -w /root/project emojijournal-run sh -c .build-ubuntu/release/EmojiJournalServer

Then enter this command:

docker exec -it emojijournal bash

This command creates an interactive terminal session running the bash Unix shell. You get a command line prompt that ends with ~/project#. At this prompt, enter this command to take a look at the container’s project directory:

ls -l

The output lists the same files as the EmojiJournalServer folder in Finder:

Use your favorite text editor to create a new file in this folder — for example, kilroy.txt containing the text “I was here.” At the ~/project# prompt, enter ls -l again, to see your new file in the container. Enter this command at the ~/project# prompt:

rm kilroy.txt

In Finder, the file disappears. That’s how easy it is to connect a local directory to a container — changes in one place affect the other!

Cleanup: Stop and remove the container.

Choosing Between Volume & Bind Mount

You’ve now seen two different ways to mount host directories in a container: couchdbVolume is a volume, while the EmojiJournalServer directory is a bind mount. The main differences between a volume and a bind mount are where it lives on the host file system, and whether you can access it directly through the host file system.

You bind-mount a specific path on your local system — like the EmojiJournalServer folder — to a specific path in the container — /root/project. You can interact directly with the local directory and changes will appear in the container. Likewise, changes made by the container appear in Finder.

You create a volume by specifying a name, like couchdbVolume, in either a docker run or a docker volume create command. The volume lives in Docker’s storage directory, in the Docker engine on your local system. You cannot interact directly with the contents of this directory. You can mount it into containers, and you can inspect its metadata or remove it. Docker manages reading and writing files in the volume.

Another big difference is what happens when you expose an existing container directory.

If you bind-mount a local directory to an existing container directory, what’s in the local directory obscures the container directory’s contents — this is what happens if you bind-mount your local couchdb-config directory to the CouchDB container’s local.d directory. You effectively overwrite the container directory. Don’t worry, the original directory is still in the read-only image!

If you expose a container directory as a volume, its contents are copied into the volume on the host. This is what happened when you created couchdbVolume with the CouchDB container’s /opt/couchdb/data directory as its target.

--mount or --volume?

There are also two different options you can use to mount volumes or bind-mount local directories. They do the same thing — they just use different syntax.

The --mount option is newer than --volume. It uses comma-separated key=value pairs, and the order of the keys doesn’t matter. This option doesn’t have a shorthand version.

The --volume flag, shorthand -v, has three colon-separated fields: the order matters, but you have to remember, or look up, the meaning of each field.

Docker documentation recommends using the --mount option instead of the -v option: it’s more verbose, but that makes it easier to understand and remember.

The following two commands are equivalent — they bind-mount the current local directory to a new /home directory in the container:

docker run --mount type=bind,source=/`pwd`,target=/home ibmcom/kitura-ubuntu
docker run -v $PWD:/home ibmcom/kitura-ubuntu

The following two commands are equivalent — they create a volume in Docker’s storage directory:

docker run --mount source=KituraVolume,target=/Kitura-Starter ibmcom/kitura-ubuntu
Note: type=volume is the default.
docker run -v KituraVolume:/Kitura-Starter ibmcom/kitura-ubuntu
Note: The first field of the -v option is a name, not a path, so Docker creates a volume, not a bind mount.

Both commands copy /Kitura-Starter‘s contents into a volume named KituraVolume. The Kitura container then mounts and uses KituraVolume. You can mount KituraVolume in other containers, to give them access to its contents.

You can specify read-only access:

docker run --mount source=KituraVolume,target=/Kitura-Starter,readonly --name kitura ibmcom/kitura-ubuntu
docker run -v KituraVolume:/Kitura-Starter:ro --name kitura ibmcom/kitura-ubuntu

Here’s yet another difference between the behavior of --mount and --volume, from Docker’s documentation:

  • If you use -v or --volume to bind-mount a file or directory that does not yet exist on the Docker host, -v creates the endpoint for you. It is always created as a directory.
  • If you use --mount to bind-mount a file or directory that does not yet exist on the Docker host, Docker does not automatically create it for you, but generates an error.

Note that if you use -v to bind-mount a directory that doesn’t exist on the host, to a directory that exists in the container, it will be created as an empty directory on the host, so its emptiness will obscure the original contents in the container. For example, if you try to copy the Kitura-Starter directory from the container to your local file system with -v $PWD/kitura-dir:Kitura-Starter, all you’ll get are empty directories in both your file system and the container.

Copying Files Between Host & Container

If you just want to save a file or directory from a running Docker container to your local system, you can just copy it:

docker cp kitura:/Kitura-Starter . 

You can also copy a file or directory from your local system to a container:

docker cp kilroy.txt kitura:/var/tmp 

Summary of Docker Commands

You’ve seen a lot of Docker commands in this tutorial! Below is a list of them. Terms in square brackets, like [image], are placeholders for specific names or IDs.

The docker run command:

docker run [options] [image] [command for -it options]

The docker run options you’ve used in this tutorial:

- --detach / -d
- --env / -e [ENV_VAR=value] 
- --interactive --tty / -it ... [command]
- --name [name]
- --network [network]
- --mount source=[volume],target=[container-dir]
- --publish / -p [host port]:[container port]
- --volume / -v [host-dir]:[container-dir]

The other Docker commands you’ve used in this tutorial (plus one or two related commands you haven’t seen):

- docker pull [image]
- docker images or docker image ls
- docker ps -a or docker container ls -a
- docker ps -a -q -f [filter condition]
- docker exec -it [container] bash
- docker stop [container]
- docker start [container]
- docker rm $(docker ps -a -q -f status=exited)
- docker rm $(docker stop $(docker ps -q))
- docker rm [container IDs or names] 
- docker rmi [image] or docker image rm [image]
- docker container inspect [container]
- docker network create [network-name]
- docker network inspect [network-name]
- docker network ls
- docker network prune
- docker volume create [volume-name]
- docker volume inspect [volume-name]
- docker volume ls
- docker volume prune
- docker cp [container:container-path] [host-path]
- docker cp [host-path] [container:container-path] 

Some useful ways to detach containers running in the foreground:

  • Control-P-Q detaches without stopping the container
  • If the container is in the foreground because you ran docker run -it ... bash, exit at the bash shell prompt stops the container.
  • If the container is in the foreground because you ran docker exec -it ... bash, exit at the bash shell prompt exits the bash shell but doesn’t stop the container.

Where to Go From Here?

This tutorial introduced you to a tiny fraction of the capabilities and possibilities of Docker. It hasn’t touched on creating your own Docker images, composing Docker services, using Docker for CI/CD, clustering and scheduling containers with Docker Swarm, and more. Hopefully this tutorial has given you a solid foundation for exploring the Docker universe. Here are some resources to keep you going:

We hope you enjoyed this tutorial on Docker and, if you have any questions or comments, please join the forum discussion below!

Contributors

Comments