In the previous part of this Docker tutorial series, we dockerised a PHP app with PostgreSQL database. We noted, however, that upon removal of Postgres container, the data was lost which should not be the case. We also noted for the app container that the image must be built again for the updated code to be synced with the code running in container. Therefore that Docker setup was also not sufficient for local development. In this tutorial, we’ll solve both of those issues by using bind mounts in both Docker containers.
Understanding the database persistence problem
If you are here as a result of Google search then probably you know the data persistence problem. But if you haven’t faced/realised it yet do the following steps for understanding. Please refer to Run a PostgreSQL Container section of the previous part if you don’t understand any of the steps.
- Run a Postgres container with following command.
> docker container run -p 5433:5432 --network dock-net --name pg-container -e POSTGRES_PASSWORD=1234 postgres:12.2
- Connect to this database host using any database client application like PGAdmin.
- Create a database
testdb
. - Stop and remove the container with following commands.
> docker container stop pg-container
> docker container rm pg-container
The container gets removed.
- Now run Postgres container again with same command and connect to DB host in your database client app.
> docker container run -p 5433:5432 --network dock-net --name pg-container -e POSTGRES_PASSWORD=1234 postgres:12.2
- You’ll see that
testdb
that you created above is no more available. The reason is that the database storage directory was owned by the container. Therefore when the container was removed, that data directory was also removed.
This is not an ideal situation if you are using Docker setup in production or even in development. You’d not want to setup your DB again, if you had to remove the container for any reason.
Solution: Data persistence with bind mount volume
The solution is quite easy and simple.
- Run following command in Terminal. (If a container with name
pg-container
exists, remove that container firstdocker container rm pg-container --force
)
> docker container run \
-p 5433:5432 \
--network dock-net \
--name pg-container \
-e POSTGRES_PASSWORD=1234 \
--mount type=bind,target=/var/lib/postgresql/data,source=/path/to/folder/on/host \
postgres:12.2
This command is same as before except that we have used --mount
option and that we have split the command over multiple lines. --mount
option has several components but we need to use only three here.
For all other options used in above docker container run
command you can see explanation in the previous tutorial. Below is the explanation of parameter used with --mount
option.
source=/path/to/folder/on/host
Create/use a folder on your host machine where you want to persist your databases. source
should have the absolute path to that folder.
target=/var/lib/postgresql/data
This is the path to default data directory of Postgres. As Postgres is running inside Docker container, this path is not host path but container’s.
type=bind
type
can have value bind
, volume
or tmpfs
. For bind mount we must use type=bind
. With this, the source
folder will be mounted into the container
and will bind
to the target
path. This means that the source
folder will be accessible to Postgres on container’s internal path specified as target
. The source
folder is managed by host and removing the container will not remove it.
The following image from official Docker docs shows how bind mount works compared to other types.
- In your Postgres client app, connect to this DB host.
- Create a database
testdb
. stop
and remove (rm
) the container.
> docker container stop pg-container
> docker container rm pg-container
- Now, create the container again with the same command with bind mount.
> docker container run \
-p 5433:5432 \
--network dock-net \
--name pg-container \
-e POSTGRES_PASSWORD=1234 \
--mount type=bind,target=/var/lib/postgresql/data,source=/path/to/folder/on/host \
postgres:12.2
- Connect to DB host again in your Postgres client app (eg. PGAdmin), you’ll see that the database
testdb
is still available this time.
Now you can safely use Docker and even remove the Postgres container without worrying about data loss!
Problem: Need to docker build
on every code update
In previous tutorial we Dockerised a Python app which runs perfectly fine with the code provided at build time. However, if we have to make changes to code, for example, to add a new API endpoint. In such case, we have to run docker build
command again to see the impact of new changes. This is not a feasible approach if you are developing app in dockerised environment.
To understand and experience this problem, complete the application setup as done in previous tutorial: Dockerise a Python app with PostgreSQL. Once you have verified that the app is running, go ahead and make some changes in api.py
. For example, add a new function as below:
@app.route('/api/welcome', methods=['GET'])
def index():
return 'welcome!'
If you hit in your browser or Postman this URL: http://127.0.0.1:8000/api/welcome, you’ll get 404 / Not Found
error because the changes in code were made on host but the container doesn’t know about those changes.
Solution: Bind mount the application directory
To solve this problem, the solution is same as we did for database persistence. Therefore execute the following command (remove flask-api-exp
container, if it already exists, using command: docker container rm flask-api-exp --force
and then execute…)
docker container run \
-p 8000:5000 \
--network dock-net \
--name flask-api-exp \
--mount type=bind,target=/dockerised-example,source=/host/path/to/flask-api-example \
flask-docker-exp:latest
The command is using --mount
exactly the same way as explained above. With type=bind
, it bind-mounts the source
folder from host to the target
folder path in container. This means that target
path is actually referencing source
path. Therefore any changes made in the content of source
are also reflected in target
right away.
Now the container is running! To verify that the problem is solved, please add following code again in api.py
.
@app.route('/api/welcome', methods=['GET'])
def index():
return 'welcome!'
Now hit http://127.0.0.1:8000/api/welcome in your browser or Postman. You will see Welcome! in response which means that the bind mount is working fine 🙂
What’s next!
The next exciting step is to manage the Docker containers using docker-compose which is really helpful tool in setting up development environment using Docker.