Skip to content
Switch branches/tags

Latest commit


Git stats


Failed to load latest commit information.
Latest commit message
Commit time

Personal Infrastructure As A Service

βœ… See this blogpost and this follow-up for a complete (and technical) explanation.

Services :

  • Standard notes β€” A free, open-source, and completely encrypted notes app
  • Cozy Cloud (Drive and settings only) β€” A smart personal cloud to gather all your data
  • Passbolt β€” A free, open-source, extensible, OpenPGP-based password manager
  • X-browser Sync β€” A free and open-source browser syncing tool
  • Davis β€” A MIT Cal and CardDAV server, based on sabre/dav
  • Wekan β€” A MIT Kanban board manager, comparable to Trello
  • Syncthing β€” A continuous file synchronization program under the Mozilla Public License 2.0 license
  • kvtiles β€” An open-source map tiles server in Go, Apache 2.0 License
  • Cryptpad β€” An AGPLv3 encrypted collaboration suite
  • OpenSMTPd β€” an ISC implementation of the SMTP protocol
  • Dovecot β€” a LGPLv2.1 / MIT robust IMAP server

All services are served through the Træfik reverse-proxy, certificates are provided by Let's Encrypt, and renewed automatically via Træfik.


Source the env vars needed for OpenStack


Create the machine

docker-machine create -d openstack \
--openstack-flavor-name="b2-7" \
--openstack-region="GRA5" \
--openstack-image-name="Debian 9" \
--openstack-net-name="Ext-Net" \
--openstack-ssh-user="debian" \
--openstack-keypair-name="MY_KEY_NAME_IN_OPENSTACK" \
--openstack-private-key-file="/path/to/.ssh/id_rsa" \

Install necessary packages on the host

docker-machine ssh default 'sudo apt update && sudo apt install -y -f software-properties-common fail2ban haveged'
  • software-properties-common is a common package providing standard libs
  • fail2ban is to prevent unwanted access
  • haveged is for Passbolt - to generate entropy

Mount external attached block storage volume

The volumes must be attached beforehand in the OpenStack console

The databases volume :

docker-machine ssh default 'sudo fdisk /dev/sdb # n, p, w'
docker-machine ssh default 'sudo mkfs.ext4 /dev/sdb1'
docker-machine ssh default 'sudo mkdir /mnt/databases && sudo mount /dev/sdb1 /mnt/databases'
docker-machine ssh default 'sudo mkdir /mnt/databases/mysql /mnt/databases/couch /mnt/databases/mongo'

The files volume :

docker-machine ssh default 'sudo fdisk /dev/sdc # n, p, w'
docker-machine ssh default 'sudo mkfs.ext4 /dev/sdc1'
docker-machine ssh default 'sudo mkdir /mnt/files && sudo mount /dev/sdc1 /mnt/files'
docker-machine ssh default 'sudo mkdir /mnt/files/cozy /mnt/files/sync /mnt/files/cryptpad /mnt/files/mails'
For mails, ensure that the permissions are correct
docker-machine ssh default 'sudo chown :$MAIL_VOLUME_GROUP /mnt/files/mails'
docker-machine ssh default 'sudo chmod 775 /mnt/files/mails' # Full access to members of the group
docker-machine ssh default 'sudo chmod g+s /mnt/files/mails' # Ensure all future content in the folder will inherit group ownership

Get environment variables to target the remote docker instance

eval $(docker-machine env default)

Alternatively, you can create a context :

First, get the host from your docker-machine env:

docker-machine env | grep HOST

Which will return something like:

export DOCKER_HOST="tcp://xx.yy.zz.aa:2376"

Use this remote host to create a new context (you can name it how you like, I used cloud here):

docker context create cloud --docker "host=tcp://xx.yy.zz.aa:2376,cert=~/.docker/machine/certs/cert.pem,key=~/.docker/machine/certs/key.pem,ca=~/.docker/machine/certs/ca.pem"

Then, you just have to docker context use cloud before being able to run commands as usual.

You will find all your contexts with docker context ls :

$ docker context ls
NAME                DESCRIPTION                               DOCKER ENDPOINT               KUBERNETES ENDPOINT   ORCHESTRATOR
cloud *                                                       tcp://xx.yy.zz.aa:2376
default             Current DOCKER_HOST based configuration   unix:///var/run/docker.sock                         swarm

Pay attention! docker-compose does not know of contexts ...

Init all submodules to retrieve up to date code

git submodule update --init

When rebuilding, don't forget to update submodules with git submodule update --recursive --remote

Build all custom images

Build configuration files first (so that environment variables are replaced correctly):


And then build the images :

docker-compose build

If you want to extend the Docker Compose services definitions, you can create an addendum docker-compose.supplementary.yaml file for instance, and run docker-compose using both files to merge the configurations:

docker-compose -f docker-compose.yaml -f docker-compose.supplementary.yaml ps

You can check that your configuration is merged correctly with:

docker-compose -f docker-compose.yaml -f docker-compose.supplementary.yaml config

See this Medium post for more details

Set the Cozy instance


NB: To add an app later on (e.g. the Notes app), you can: docker exec -it cozy bash and then cozy-stack apps install --domain ${CLOUD_DOMAIN} notes registry://notes/stable

Provision the whole thing in daemon mode

docker-compose up -d


Create the Passbolt admin user


Init the davis instance if needed (if the tables do not already exist)


And finally, create a rule so that all the traffic of mail containers (SMTPD mainly) goes out by the MAIL_HOST_IP defined in your .env file


⚠️ WARNING ⚠️ : On Debian Buster (10), iptables now uses nft under the hood, and it just doesn't work in this case. You need to select the legacy iptables via update-alternatives --config iptables first, restart the Docker engine, and recreate the networks (so that the rules are re-applied) before playing the script above. See for instance

Automatic backups

In the event of a burning datacenter, you might want to backup all your data to some other provider / server so that you can recover (most of) your data.

We're going to incrementally backup /mnt/database and /mnt/files β€”Β that should be sufficient to help us recover from a disaster.

We use duplicity for this, and a S3-compatible backend to store the backups remotely (but with duplicity, you can use pretty much whatever service you want).

See for more info on their Object Storage solutions and the way it works with duplicity

Install Python 3.9.2 and the latest duplicity version

On the Docker host:

Install Python 3.9.2

sudo apt install --no-install-recommends wget build-essential libreadline-gplv2-dev libncursesw5-dev \
 libssl-dev libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev zlib1g-dev

tar xzf Python-3.9.2.tgz
cd Python-3.9.2
./configure --enable-optimizations
sudo make install # with 'sudo', we replace the original Python provided with the distro

Install Duplicity requirements

sudo apt update && sudo apt install -y -f gettext librsync-dev

Compile and install Duplicity with latest Python3.9 (that we previously installed)

tar xaf duplicity-0.8.18.tar.gz
cd duplicity-0.8.18
sudo pip3.9 install -r requirements.txt
sudo pip3.9 install boto # for S3 remote target
sudo python3.9 install

You must create a /root/.aws/credentials file with your S3 credentials:


The user in which "home" you set these credentials will need to be the one running the cron task obviously. A simple solution would be to use root, since duplicity must be able to read all the files that you want to backup

Add a crontab for the backup

Create /etc/cron.d/backup_daily with :

42 01 * * * root duplicity incr --full-if-older-than 365D --volsize 1024 --asynchronous-upload --no-encryption --include /mnt/databases --include /mnt/files --exclude '**' /mnt/ s3://<S3_HOST>/<S3_BUCKET_NAME> >> /var/log/duplicity.log 2>&1

This will run every day, at 01:42 AM, as the root user.

Options (see

  • --volsize 1024 : Use chunks of 1Go
  • --asynchronous-upload : Try to speed up uploads using CPU and bandwidth more efficiently
  • --no-encryption : Do not encrypt remote backups
  • --include /mnt/databases --include /mnt/files --exclude '**' : Only backup /mnt/files and /mnt/databases

Bonus: additional cli commands to work on backups

List all backed-up files

duplicity list-current-files s3://<S3_HOST>/<S3_BUCKET_NAME>

Verify data (in depth) and its recoverability

duplicity verify \
    --no-encryption \
    --include /mnt/databases \
    --include /mnt/files \
    --exclude '**' \
    --compare-data \
    s3://<S3_HOST>/<S3_BUCKET_NAME> /mnt/


Update Dockerfiles or the docker-compose.yml file, then rebuild the images with docker-compose build. You can then recreate each container with the newly built images with docker-compose up -d {container}.

For some containers using a shared volume such as Davis (/var/www/davis), you need to scrap the underlying volume before updating so that the code is really updated.

For instance:

docker rm -f davis davis-proxy && docker volume rm davis_www
docker container prune && docker image prune
docker-compose up -d --force-recreate --build davis-proxy davis


The given Traefik V2.0 configuration (SSL params, etc), along with a proper DNS configuration (including a correct CAA entry β€” see here), will result in a A+ rating in SSLLabs :

A+ Rating page

DNS entries for mail

You have to add some DNS entries to make your setup work. Run the following scripts to have them listed according to your environment values:


Test your email server

Test that your SMTP endpoint works as expected:

openssl s_client -starttls smtp -connect


openssl s_client -connect

Both should yield a prompt, and say that the certificate is ok (Verify return code: 0 (ok))

Test your IMAP endpoint (Dovecot) with:

openssl s_client -connect

You can try to login with A LOGIN {user}Β {password} by replacing {user} and {password} with the real strings, which should yield something along those lines:


Run & Maintenance

To prevent user registration in the notes container :

docker exec -it notes sed -i 's/\(post "auth" =>\)/# \1/' /data/src/config/routes.rb
docker-compose restart standardnotes

To prevent user registration in wekan, just go in the settings page (https://{}/setting) and deactivate it.

To see the disk usage :

docker-machine ssh default "df -h | grep '^/dev'"

When making a block storage bigger :

  1. First stop the container using it (cozy + syncthing for instance, or many more if it's the databases)
  2. Unmount the /dev/sd*1 volume
  3. Change the size in the Public Cloud interface
  4. WARNING The volume name will likely change
  5. sudo fdisk /dev/sd* (no number here): Delete (d,w) / recreate the partition (n,p,w) / sudo e2fsck -f /dev/sd*1 / sudo resize2fs /dev/sd*1
  6. Remount it
  7. Restart the container
  8. πŸŽ‰

See for more info


If you change, you need to clear the content of /mnt/databases/mysql (mongo, or couch too if needed) on the host for the entrypoint script to be replayed entirely

Add a failover IP on Debian 9

Supposing an alias of 1, and an interface of ens3 :

Disable auto configuration on boot by adding :

network: {config: disabled}

in /etc/cloud/cloud.cfg.d/99-disable-network-config.cfg

Edit /etc/network/interfaces.d/50-cloud-init.cfg and add :

auto ens3:1
iface ens3:1 inet static

The map tiles server

You can change the region, just grab a tag at, such as france-13-latest for instance.

The tiles server is available directly at https://{MAPS_DOMAIN}/. You can see a handy map at https://{MAPS_DOMAIN}/static/?key={MAPS_API_KEY}.

How-to rename a docker volume

echo "Creating destination volume ..."
docker volume create --name new_volume_name
echo "Copying data from source volume to destination volume ..."
docker run --rm \
           -i \
           -t \
           -v old_volume_name:/from \
           -v new_volume_name:/to \
           alpine ash -c "cd /from ; cp -av . /to"

How to disable ipv6 on Debian

You might need this if Traefik does not manage to get certificates with a tls challenge (and if you don't have any ipv6 dns created)

sysctl -w net.ipv6.conf.all.disable_ipv6=1
sysctl -w net.ipv6.conf.default.disable_ipv6=1
sysctl -w net.ipv6.conf.lo.disable_ipv6=1



Dockerfiles :

Other alternatives

See for more awesome self-hosted alternatives.

Other CalDav / CardDav projects worth noting

About the tiles server


🐳 β›… Your own private cloud services with Docker







No releases published


No packages published