Container as an external hard drive: sharing a common directory (BitTorrent style) using Docker Swarm

The question: How can we share a common directory between our nodes easily? In other words, how can we make our app stateful in a cluster? I described the challenge over our Technical Challenge post.

Requirement & specs

As a DevOps hero:

  • As a DevOps hero, I'm looking for a private ZFS / GlusterFS server or whatever application that mounts a common directory between all my nodes.
  • As a DevOps hero, I want to have a common directory (not a docker volume) that all nodes can share. I suspect that having hundreds of docker volume will slow down over time. Something like /mnt/shared/permdata/
  • As a DevOps hero, I want to run the solution as a docker service create (...). No manual configs on each node and especially no hard IP to set up.
  • As a DevOps hero, I want to create a new node on my existing cluster. The data should sync automatically.
  • As a DevOps hero, I have a 3 nodes set up on docker swarm
  • As a DevOps hero, everything needs to happen via the CLI (no GUI operation)
  • No need external sync to the cloud (like AWS s3)
  • The traffic must use the swarm ingress, not via public traffic
  • Don't use a "volume" or a plug-in. The container must do the work of synching.

Per example, I would use it this way where permdata is the common directory :

/mnt/shared/permdata/app1/
/mnt/shared/permdata/app2/
/mnt/shared/permdata/bkp/
/mnt/shared/permdata/etc/

Solution

I use Resilio Sync since over a year and it's perfectly stable.

Performances: I tested it a lot! Most of the time, a file like a SQLite3 myapp.db or a picture get sync under 5 seconds. Sometimes it takes longer but most of the time it's fast.

Private network

UPDATE (2020-02-25): As expected, by default Resilio will use the public network. The good news is that you can set a configuration file to limit the network to eht0. You might know that on Digital Ocean the private network is happening over eth0. This way our network requirement is meet!

The stack

  • Have a docker swarm cluster running (3 nodes in my example).
  • Define our VAR:
MNT_SOURCE_RESILIO="/mnt/shared/permdata"
IMG_resilio="devmtl/resilio:2.6.3"
CTN_resilio1="node1"
CTN_resilio2="node2"
CTN_resilio3="node3"
  • Create this network:
NTW_RESILIO="ntw_resilio"

if [ ! "$(docker network ls --filter name=${NTW_RESILIO} -q)" ]; then
  docker network create --driver overlay --attachable --subnet 10.23.10.0/24 --opt encrypted ${NTW_RESILIO}
else
  echo "Network: ${NTW_RESILIO} already exist!"
fi
  • Create those labels:
docker node update --label-add nodeid=1 node1
docker node update --label-add nodeid=2 node2
docker node update --label-add nodeid=3 node3

How to

Step #1

Create a token:

docker run -dit \
-v $(pwd)/data:/data \
-p 33333:33333 \
"$IMG_resilio" && docker ps;

# find the secret
docker logs -f NAME; echo;

Find the TOKEN that looks like: A2QYBAQPK7SOP4O4ETFJEFHO5VLGHE747
What I do then is to copy this token on my nodes outside my git repo (along other secrets). In my case the file is here:

${MNT_DEPLOY_SETUP}/config/resilio/token

Delete this container. It was only to generate a token.

Step #2

Launch a Resilio Sync instance on each node.

Warning: Here we can't use --global because we need to configure different ports on each instance. This is because our service is using our public IP to sync data.

# First host
docker service rm ${CTN_resilio1}; \

docker service create \
  --name ${CTN_resilio1} --hostname ${CTN_resilio1} \
  --network ${NTW_RESILIO} --replicas "1" \
  --restart-condition "any" --restart-max-attempts "20" \
  --reserve-memory "192M" --limit-memory "512M" \
  --limit-cpu "0.333" \
  --constraint 'node.labels.nodeid == 1' \
  --publish "33331:33333" \
  -e RSLSYNC_SECRET=$(cat ${MNT_DEPLOY_SETUP}/config/resilio/token) \
  --mount type=bind,source=${MNT_SOURCE_RESILIO},target=/data \
  ${IMG_resilio}


# Second host
docker service rm ${CTN_resilio2}; \

docker service create \
  --name ${CTN_resilio2} --hostname ${CTN_resilio2} \
  --network ${NTW_RESILIO} --replicas "1" \
  --restart-condition "any" --restart-max-attempts "20" \
  --reserve-memory "192M" --limit-memory "512M" \
  --limit-cpu "0.333" \
  --constraint 'node.labels.nodeid == 2' \
  --publish "33332:33333" \
  -e RSLSYNC_SECRET=$(cat ${MNT_DEPLOY_SETUP}/config/resilio/token) \
  --mount type=bind,source=${MNT_SOURCE_RESILIO},target=/data \
  ${IMG_resilio}

# Third host
docker service rm ${CTN_resilio3}; \

docker service create \
  --name ${CTN_resilio3} --hostname ${CTN_resilio3} \
  --network ${NTW_RESILIO} --replicas "1" \
  --restart-condition "any" --restart-max-attempts "20" \
  --reserve-memory "192M" --limit-memory "512M" \
  --limit-cpu "0.333" \
  --constraint 'node.labels.nodeid == 3' \
  --publish "33333:33333" \
  -e RSLSYNC_SECRET=$(cat ${MNT_DEPLOY_SETUP}/config/resilio/token) \
  --mount type=bind,source=${MNT_SOURCE_RESILIO},target=/data \
  ${IMG_resilio}

Step #3

Now it's time to test it. From node1, put a file in ${MNT_SOURCE_RESILIO}. Wait a few seconds. Check on node2 and node3.

The file should appear quickly :)

Step #4

You might want to remove files and directories in the /mnt/shared/permdata/.sync/Archive directory as Resilio Sync will archive everything by default.

I have a crontab script that cleans this directory every hour.

Step #5

It's now time to build your own docker image with this Dockerfile. I shared my project over https://github.com/firepress-org/resilio-in-docker

Feel free to Buzz me on Twitter or in the Github repo.

Cheers!
Pascal