Docker Swarm Persistent Storage with NFS

In this tutorial we will experiment with Docker Swarm Persistent Storage, backed by NFS using ContainX's Netshare Service

Get the dependencies:

If you have not provisioned a swarm using docker, have a look at setup a 3 node docker swarm post.

In order to mount NFS drives we need to install the following:

$ apt install nfs-common -y

Create NFS Server:

We have 2 options, we can either install a NFS Server (you can follow this post to setup a NFS Server )  or we can setup a NFS Server using Docker (explained below):

Prepare the NFS Home Directory:

$ mkdir /nfsdata

I would like to restrict NFS traffic to a specific interface, so I will use my private interface for my NFS traffic.

Create the NFS Server:

$ docker run --rm -itd --name nfs \
  --privileged \
  -v /nfsdata:/nfs.1 \
  -e SHARED_DIRECTORY=/nfs.1 \
  -p 10.0.2.15:2049:2049 \
  itsthenetwork/nfs-server-alpine:latest

Testing NFS

Test that NFS works, by creating a directory on the NFS home path and mount the NFS volume:

$ mkdir /nfsdata/test_folder
$ mount -v -t nfs4 -o vers=4,loud 10.0.2.15:/ /nfs_share
$ ls /nfs_share
test_folder

Now that we can see the directory via the NFS share, we can umount the NFS share, as the docker volume driver will mount the path inside the container:

$ umount /nfs_share

Install Netshare Docker Volume Driver

Install Netshare which will provide the NFS Docker Volume Driver:

$ wget https://github.com/ContainX/docker-volume-netshare/releases/download/v0.36/docker-volume-netshare_0.36_amd64.deb
$ dpkg -i docker-volume-netshare_0.36_amd64.deb
$ service docker-volume-netshare start

Create the NFS Volume

With NFS, everytime we create a NFS Volume, the path to that directory need to exit.

A couple of ways to create volumes:

  1. Run a container with a persistent NFS backed volume (directory must exist)

Create the directory:

$ mkdir /nfsdata/foobar

Run the container:

$ docker run -i -t --volume-driver=nfs -v 10.0.2.15/foobar:/mount alpine /bin/sh

Inspect the filesystem in the container:

$ df -h | grep -e 'Filesystem\|mount'
Filesystem                Size      Used Available Use% Mounted on
10.0.2.15://foobar        9.1G      1.7G      6.8G  20% /mount

2. Create the volume and map the volume name:

Create the directory:

$ mkdir /nfsdata/foobar2

Create the docker volume:

$ docker volume create --driver nfs --name foobar2 -o share=10.0.2.15:/foobar2

Inspect the volume:

$ docker volume inspect foobar2
[
    {
        "CreatedAt": "0001-01-01T00:00:00Z",
        "Driver": "nfs",
        "Labels": {},
        "Mountpoint": "/var/lib/docker-volumes/netshare/nfs/foobar2",
        "Name": "foobar2",
        "Options": {
            "share": "10.0.2.15:/foobar2"
        },
        "Scope": "local"
    }
]

Run a container with the volume namespace:

$ docker run -i -t -v foobar2:/mount alpine /bin/sh

Demo: Using docker compose to persist nginx data in docker swarm

Create the directory:

$ mkdir /nfsdata/nginx_web

Create the index.html that the nginx container will use:

$ echo '<html>Hello, World!</html>' > /nfsdata/nginx_web/index.html

The docker-compose.yml

version: "3.7"
services:
  web:
    image: nginx
    volumes:
      - nginx.vol:/usr/share/nginx/html
    ports:
      - 80:80
    networks:
      - web

networks:
  web:
    driver: overlay
    name: web

volumes:
  nginx.vol:
    driver: nfs
    driver_opts:
      share: 10.0.2.15:/nginx_web

Deploy the application to the swarm:

$ docker stack deploy -c docker-compose.yml app

Test nginx:

$ curl -i http://localhost/
HTTP/1.1 200 OK
Server: nginx/1.17.1
Date: Tue, 23 Jul 2019 09:35:32 GMT
Content-Type: text/html
Content-Length: 27
Last-Modified: Tue, 23 Jul 2019 09:35:21 GMT
Connection: keep-alive
ETag: "5d36d4d9-1b"
Accept-Ranges: bytes

<html>Hello, World!</html>

We can see the content is being served from our persistent NFS volume, let's change the index.html:

$ echo '<html>hello, again :D</html>' > /nfsdata/nginx_web/index.html

And test again:

$ curl -i http://localhost/
HTTP/1.1 200 OK
Server: nginx/1.17.1
Date: Tue, 23 Jul 2019 09:36:16 GMT
Content-Type: text/html
Content-Length: 29
Last-Modified: Tue, 23 Jul 2019 09:36:14 GMT
Connection: keep-alive
ETag: "5d36d50e-1d"
Accept-Ranges: bytes

<html>hello, again :D</html>

Testing persistence, remove the stack and redeploy:

$ docker stack rm app
$ docker stack deploy -c docker-compose.yml app

Test again:

$ curl -i http://localhost/
HTTP/1.1 200 OK
Server: nginx/1.17.1
Date: Tue, 23 Jul 2019 09:38:33 GMT
Content-Type: text/html
Content-Length: 29
Last-Modified: Tue, 23 Jul 2019 09:36:14 GMT
Connection: keep-alive
ETag: "5d36d50e-1d"
Accept-Ranges: bytes

<html>hello, again :D</html>

Resources:

Thanks

If you enjoyed this post feel free to check out my website for more content at ruan.dev or follow me on twitter @ruanbekker