content/get-started/workshop/06_bind_mounts.md
In part 4, you used a volume mount to persist the data in your database. A volume mount is a great choice when you need somewhere persistent to store your application data.
A bind mount is another type of mount, which lets you share a directory from the host's filesystem into the container. When working on an application, you can use a bind mount to mount source code into the container. The container sees the changes you make to the code immediately, as soon as you save a file. This means that you can run processes in the container that watch for filesystem changes and respond to them.
In this chapter, you'll see how you can use bind mounts and a tool called nodemon to watch for file changes, and then restart the application automatically. There are equivalent tools in most other languages and frameworks.
The following are examples of a named volume and a bind mount using --mount:
type=volume,src=my-volume,target=/usr/local/datatype=bind,src=/path/to/data,target=/usr/local/dataThe following table outlines the main differences between volume mounts and bind mounts.
| Named volumes | Bind mounts | |
|---|---|---|
| Host location | Docker chooses | You decide |
| Populates new volume with container contents | Yes | No |
| Supports Volume Drivers | Yes | No |
Before looking at how you can use bind mounts for developing your application, you can run a quick experiment to get a practical understanding of how bind mounts work.
Verify that your getting-started-app directory is in a directory defined in
Docker Desktop's file sharing setting. This setting defines which parts of your
filesystem you can share with containers. For details about accessing the setting, see File sharing.
[!NOTE] The File sharing tab is only available in Hyper-V mode, because the files are automatically shared in WSL 2 mode and Windows container mode.
Open a terminal and change directory to the getting-started-app
directory.
Run the following command to start bash in an ubuntu container with a
bind mount.
{{< tabs >}} {{< tab name="Mac / Linux" >}}
$ docker run -it --mount type=bind,src=.,target=/src ubuntu bash
{{< /tab >}} {{< tab name="Command Prompt" >}}
$ docker run -it --mount "type=bind,src=%cd%,target=/src" ubuntu bash
{{< /tab >}} {{< tab name="Git Bash" >}}
$ docker run -it --mount type=bind,src="/.",target=/src ubuntu bash
{{< /tab >}} {{< tab name="PowerShell" >}}
$ docker run -it --mount "type=bind,src=.,target=/src" ubuntu bash
{{< /tab >}} {{< /tabs >}}
The --mount type=bind option tells Docker to create a bind mount, where src is the
current working directory on your host machine (getting-started-app), and
target is where that directory should appear inside the container (/src).
After running the command, Docker starts an interactive bash session in the
root directory of the container's filesystem.
root@ac1237fad8db:/# pwd
/
root@ac1237fad8db:/# ls
bin dev home media opt root sbin srv tmp var
boot etc lib mnt proc run src sys usr
Change directory to the src directory.
This is the directory that you mounted when starting the container. Listing
the contents of this directory displays the same files as in the
getting-started-app directory on your host machine.
root@ac1237fad8db:/# cd src
root@ac1237fad8db:/src# ls
Dockerfile node_modules package.json package-lock.json spec src
Create a new file named myfile.txt.
root@ac1237fad8db:/src# touch myfile.txt
root@ac1237fad8db:/src# ls
Dockerfile myfile.txt node_modules package.json package-lock.json spec src
Open the getting-started-app directory on the host and observe that the
myfile.txt file is in the directory.
├── getting-started-app/
│ ├── Dockerfile
│ ├── myfile.txt
│ ├── node_modules/
│ ├── package.json
│ ├── package-lock.json
│ ├── spec/
│ └── src/
From the host, delete the myfile.txt file.
In the container, list the contents of the app directory once more. Observe that the file is now gone.
root@ac1237fad8db:/src# ls
Dockerfile node_modules package.json package-lock.json spec src
Stop the interactive container session with Ctrl + D.
That's all for a brief introduction to bind mounts. This procedure demonstrated how files are shared between the host and the container, and how changes are immediately reflected on both sides. Now you can use bind mounts to develop software.
Using bind mounts is common for local development setups. The advantage is that the development machine doesn’t need to have all of the build tools and environments installed. With a single docker run command, Docker pulls dependencies and tools.
The following steps describe how to run a development container with a bind mount that does the following:
nodemon to watch for filesystem changesYou can use the CLI or Docker Desktop to run your container with a bind mount.
{{< tabs >}} {{< tab name="Mac / Linux CLI" >}}
Make sure you don't have any getting-started containers currently running.
Run the following command from the getting-started-app directory.
$ docker run -dp 127.0.0.1:3000:3000 \
-w /app --mount type=bind,src=.,target=/app \
node:24-alpine \
sh -c "npm install && npm run dev"
The following is a breakdown of the command:
-dp 127.0.0.1:3000:3000 - same as before. Run in detached (background) mode and
create a port mapping-w /app - sets the "working directory" or the current directory that the
command will run from--mount type=bind,src=.,target=/app - bind mount the current
directory from the host into the /app directory in the containernode:24-alpine - the image to use. Note that this is the base image for
your app from the Dockerfilesh -c "npm install && npm run dev" - the command. You're starting a
shell using sh (alpine doesn't have bash) and running npm install to
install packages and then running npm run dev to start the development
server. If you look in the package.json, you'll see that the dev script
starts nodemon.You can watch the logs using docker logs <container-id>. You'll know you're
ready to go when you see this:
$ docker logs -f <container-id>
nodemon -L src/index.js
[nodemon] 2.0.20
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node src/index.js`
Using sqlite database at /etc/todos/todo.db
Listening on port 3000
When you're done watching the logs, exit out by hitting Ctrl+C.
{{< /tab >}} {{< tab name="PowerShell CLI" >}}
Make sure you don't have any getting-started containers currently running.
Run the following command from the getting-started-app directory.
$ docker run -dp 127.0.0.1:3000:3000 `
-w /app --mount "type=bind,src=.,target=/app" `
node:24-alpine `
sh -c "npm install && npm run dev"
The following is a breakdown of the command:
-dp 127.0.0.1:3000:3000 - same as before. Run in detached (background) mode and
create a port mapping-w /app - sets the "working directory" or the current directory that the
command will run from--mount "type=bind,src=.,target=/app" - bind mount the current
directory from the host into the /app directory in the containernode:24-alpine - the image to use. Note that this is the base image for
your app from the Dockerfilesh -c "npm install && npm run dev" - the command. You're starting a
shell using sh (alpine doesn't have bash) and running npm install to
install packages and then running npm run dev to start the development
server. If you look in the package.json, you'll see that the dev script
starts nodemon.You can watch the logs using docker logs <container-id>. You'll know you're
ready to go when you see this:
$ docker logs -f <container-id>
nodemon -L src/index.js
[nodemon] 2.0.20
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node src/index.js`
Using sqlite database at /etc/todos/todo.db
Listening on port 3000
When you're done watching the logs, exit out by hitting Ctrl+C.
{{< /tab >}} {{< tab name="Command Prompt CLI" >}}
Make sure you don't have any getting-started containers currently running.
Run the following command from the getting-started-app directory.
$ docker run -dp 127.0.0.1:3000:3000 ^
-w /app --mount "type=bind,src=%cd%,target=/app" ^
node:24-alpine ^
sh -c "npm install && npm run dev"
The following is a breakdown of the command:
-dp 127.0.0.1:3000:3000 - same as before. Run in detached (background) mode and
create a port mapping-w /app - sets the "working directory" or the current directory that the
command will run from--mount "type=bind,src=%cd%,target=/app" - bind mount the current
directory from the host into the /app directory in the containernode:24-alpine - the image to use. Note that this is the base image for
your app from the Dockerfilesh -c "npm install && npm run dev" - the command. You're starting a
shell using sh (alpine doesn't have bash) and running npm install to
install packages and then running npm run dev to start the development
server. If you look in the package.json, you'll see that the dev script
starts nodemon.You can watch the logs using docker logs <container-id>. You'll know you're
ready to go when you see this:
$ docker logs -f <container-id>
nodemon -L src/index.js
[nodemon] 2.0.20
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node src/index.js`
Using sqlite database at /etc/todos/todo.db
Listening on port 3000
When you're done watching the logs, exit out by hitting Ctrl+C.
{{< /tab >}} {{< tab name="Git Bash CLI" >}}
Make sure you don't have any getting-started containers currently running.
Run the following command from the getting-started-app directory.
$ docker run -dp 127.0.0.1:3000:3000 \
-w //app --mount type=bind,src="/.",target=/app \
node:24-alpine \
sh -c "npm install && npm run dev"
The following is a breakdown of the command:
-dp 127.0.0.1:3000:3000 - same as before. Run in detached (background) mode and
create a port mapping-w //app - sets the "working directory" or the current directory that the
command will run from--mount type=bind,src="/.",target=/app - bind mount the current
directory from the host into the /app directory in the containernode:24-alpine - the image to use. Note that this is the base image for
your app from the Dockerfilesh -c "npm install && npm run dev" - the command. You're starting a
shell using sh (alpine doesn't have bash) and running npm install to
install packages and then running npm run dev to start the development
server. If you look in the package.json, you'll see that the dev script
starts nodemon.You can watch the logs using docker logs <container-id>. You'll know you're
ready to go when you see this:
$ docker logs -f <container-id>
nodemon -L src/index.js
[nodemon] 2.0.20
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node src/index.js`
Using sqlite database at /etc/todos/todo.db
Listening on port 3000
When you're done watching the logs, exit out by hitting Ctrl+C.
{{< /tab >}} {{< tab name="Docker Desktop" >}}
Make sure you don't have any getting-started containers currently running.
Run the image with a bind mount.
Select the search box at the top of Docker Desktop.
In the search window, select the Images tab.
In the search box, specify the container name, getting-started.
[!TIP]
Use the search filter to filter images and only show Local images.
Select your image and then select Run.
Select Optional settings.
In Host path, specify the path to the getting-started-app directory on your host machine.
In Container path, specify /app.
Select Run.
You can watch the container logs using Docker Desktop.
You'll know you're ready to go when you see this:
nodemon -L src/index.js
[nodemon] 2.0.20
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node src/index.js`
Using sqlite database at /etc/todos/todo.db
Listening on port 3000
{{< /tab >}} {{< /tabs >}}
Update your app on your host machine and see the changes reflected in the container.
In the src/static/js/app.js file, on line
109, change the "Add Item" button to simply say "Add":
- {submitting ? 'Adding...' : 'Add Item'}
+ {submitting ? 'Adding...' : 'Add'}
Save the file.
Refresh the page in your web browser, and you should see the change reflected almost immediately because of the bind mount. Nodemon detects the change and restarts the server. It might take a few seconds for the Node server to restart. If you get an error, try refreshing after a few seconds.
Feel free to make any other changes you'd like to make. Each time you make a change and save a file, the change is reflected in the container because of the bind mount. When Nodemon detects a change, it restarts the app inside the container automatically. When you're done, stop the container and build your new image using:
$ docker build -t getting-started .
At this point, you can persist your database and see changes in your app as you develop without rebuilding the image.
In addition to volume mounts and bind mounts, Docker also supports other mount types and storage drivers for handling more complex and specialized use cases.
Related information:
In order to prepare your app for production, you need to migrate your database from working in SQLite to something that can scale a little better. For simplicity, you'll keep using a relational database and switch your application to use MySQL. But, how should you run MySQL? How do you allow the containers to talk to each other? You'll learn about that in the next section.
{{< button text="Multi container apps" url="07_multi_container.md" >}}