Davis - Installing and Configuring a WebDAV, CalDAV and CardDAV Server
Greetings!

We will deploy our own Davis to store files, calendars, and contacts on our server. Then we will connect it to Linux Mint and Android. We will run everything with Docker Compose and use the Caddy reverse proxy for HTTPS and automatic TLS certificates.

Introduction

I think personal files and data no longer need an explanation for why they belong on private servers. Deploying a full cloud service like Nextcloud is not always justified, especially for standard *DAV protocols and a simple admin panel.

For example, I store my secrets in a KeePass database on a WebDav server. This is convenient when you need access to that database from different devices and locations.

Recently, I came across Davis, a compact open-source server based on Symfony and sabre/dav. It supports three useful protocols:

Davis has a Web GUI for managing users, calendars, and address books. For a small personal installation, we can use SQLite, so we will not need a separate database server.

There are several ways to connect the file storage in Linux. For this demonstration, I will use the Nemo file manager from Linux Mint. I recently covered more flexible WebDav mounting with rclone in my article on mounting SFTP, WebDAV, and S3 network storage. Take a look.

Prerequisites

This setup assumes that we already have:

The Caddy reverse proxy will run alongside Davis in the same Compose project. It will accept external connections on ports 80 and 443 and automatically obtain a Let’s Encrypt certificate. It will proxy requests to the davis container over the internal Docker network.

Installing Davis

Connect to the server over SSH and switch to the root account:

BASH
ssh ivan@dav.r4ven.me

sudo -i
Click to expand and view more

Create the project directory and restrict access to it:

BASH
install -m 700 -d /etc/compose/davis

cd /etc/compose/davis
Click to expand and view more

Creating the Compose File

Create compose.yaml, for example, with vim:

BASH
vim ./compose.yaml
Click to expand and view more

Add the following:

YAML
  1---
  2name: davis
  3
  4networks:
  5  davis:
  6
  7volumes:
  8  davis_data:
  9    name: davis_data
 10  davis_webdav:
 11    name: davis_webdav
 12  davis_caddy_data:
 13    name: davis_caddy_data
 14  davis_caddy_config:
 15    name: davis_caddy_config
 16
 17services:
 18  davis:
 19    image: ghcr.io/tchapi/davis-standalone:5.6.3
 20    container_name: davis
 21    restart: always
 22    stop_grace_period: 30s
 23    cpus: 1
 24    mem_limit: 1g
 25    hostname: davis
 26    command:
 27      - sh
 28      - -c
 29      - |
 30        set -e
 31        mkdir -p /webdav/public /webdav/homes
 32        APP_ENV=prod bin/console doctrine:migrations:migrate --no-interaction
 33        chown -R www-data:www-data /data /webdav
 34        exec /usr/bin/supervisord -c /etc/supervisord.conf
 35    env_file:
 36      - .env
 37    volumes:
 38      - /etc/ssl/certs/ca-certificates.crt:/etc/ssl/certs/ca-certificates.crt:ro
 39      - /etc/timezone:/etc/timezone:ro
 40      - /etc/localtime:/etc/localtime:ro
 41      - davis_data:/data
 42      - davis_webdav:/webdav
 43    expose:
 44      - "9000"
 45    networks:
 46      - davis
 47    logging:
 48      driver: json-file
 49      options:
 50        max-size: "10m"
 51        max-file: "3"
 52
 53  davis_caddy:
 54    image: docker.io/caddy:2-alpine
 55    depends_on:
 56      - davis
 57    container_name: davis-caddy
 58    restart: unless-stopped
 59    stop_grace_period: 30s
 60    cpus: 1
 61    mem_limit: 1g
 62    hostname: davis-caddy
 63    env_file: ./.env
 64    configs:
 65      - source: Caddyfile
 66        target: /etc/caddy/Caddyfile
 67    ports:
 68      - "80:80/tcp"
 69      - "443:443/tcp"
 70      - "443:443/udp"
 71    volumes:
 72      - /etc/timezone:/etc/timezone:ro
 73      - /etc/localtime:/etc/localtime:ro
 74      - davis_caddy_data:/data
 75      - davis_caddy_config:/config
 76    networks:
 77      - davis
 78    logging:
 79      driver: json-file
 80      options:
 81        max-size: "10m"
 82        max-file: "3"
 83
 84configs:
 85  Caddyfile:
 86    content: |
 87      {
 88          email ${ACME_EMAIL}
 89          admin off
 90      }
 91
 92      ${DAVIS_DOMAIN} {
 93          encode zstd gzip
 94
 95          reverse_proxy davis:9000
 96
 97          header {
 98              Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
 99              X-Content-Type-Options "nosniff"
100              X-Frame-Options "SAMEORIGIN"
101              Referrer-Policy "no-referrer"
102              -Server
103          }
104      }
Click to expand and view more

In this setup, the standalone Davis image contains a web server and php-fpm. A separate Caddy container handles external HTTPS. Davis port 9000 is available only inside the Docker network and is not published on the host.

The Caddy configuration lives directly in compose.yaml, in the configs section. The ACME_EMAIL and DAVIS_DOMAIN variables populate Caddyfile from .env. The reverse_proxy davis:9000 directive forwards requests to Davis by its service name.

The data is split across four Docker volumes:

At each startup, the Davis container creates the required directories, applies database migrations, and checks permissions. This removes the need to run migrations manually during updates.

Configuring Environment Variables

Create .env:

BASH
vim ./.env
Click to expand and view more

Add the following:

INI
TZ=Europe/Moscow
DAVIS_DOMAIN=dav.r4ven.me
ACME_EMAIL=kar-kar@r4ven.me
APP_ENV=prod
APP_TIMEZONE=Europe/Moscow
CALDAV_ENABLED=true
CARDDAV_ENABLED=true
WEBDAV_ENABLED=true
PUBLIC_CALENDARS_ENABLED=true
BIRTHDAY_REMINDER_OFFSET=PT9H
ADMIN_LOGIN=ivan
ADMIN_PASSWORD=r4ven_me
ADMIN_AUTH_BYPASS=false
AUTH_METHOD=Basic
AUTH_REALM=SabreDAV
WEBDAV_TMP_DIR=/tmp
WEBDAV_PUBLIC_DIR=
WEBDAV_HOMES_DIR=/webdav/homes
INVITE_FROM_ADDRESS=no-reply@r4ven.me
MAILER_DSN=null://null
SYMFONY_TRUSTED_PROXIES=REMOTE_ADDR
DATABASE_DRIVER=sqlite
DATABASE_URL=sqlite:////data/davis_database.db
Click to expand and view more

Main parameters:

Generate the administrator password:

BASH
ADMIN_PASSWORD="$(openssl rand -base64 32 | tr -d '\n')"

sed -i "s|^ADMIN_PASSWORD=.*|ADMIN_PASSWORD=${ADMIN_PASSWORD}|" ./.env
Click to expand and view more

And restrict access to the configuration:

BASH
chmod 600 ./.env ./compose.yaml
Click to expand and view more

In this example, MAILER_DSN=null://null disables email invitations. You will need a working SMTP service to invite event participants. We will configure it below.

Starting the Container

Check the final configuration:

BASH
docker compose config
Click to expand and view more

Start the stack:

BASH
docker compose up -d

docker compose logs -f
Click to expand and view more

Press Ctrl+c to stop viewing the logs. Then check the container status:

BASH
docker compose ps
Click to expand and view more

Configuring HTTPS with Caddy

After startup, Caddy reads the embedded configuration in compose.yaml, Caddyfile, and contacts the Let’s Encrypt ACME server. It requests a certificate for the domain specified by DAVIS_DOMAIN. It stores certificates and keys in davis_caddy_data, so ordinary container recreation does not trigger new issuance.

Automatic certificate issuance requires three conditions:

Check the Caddy log:

BASH
docker compose logs davis_caddy
Click to expand and view more

The log should report that Caddy successfully obtained the certificate. Then open https://dav.r4ven.me/ in a browser and check the certificate:

Davis handles /.well-known/caldav and /.well-known/carddav itself, redirecting clients to /dav/. You do not need separate rewrite rules in Caddyfile.

Check external access:

BASH
curl -I https://dav.r4ven.me/

curl -I https://dav.r4ven.me/.well-known/caldav

curl -I https://dav.r4ven.me/.well-known/carddav
Click to expand and view more

The well-known addresses should redirect to the main /dav/ endpoint. If the certificate is not ready, first check docker compose logs davis_caddy, the DNS record, and ports 80 and 443.

Initial Davis Setup

Open the administration panel address in a browser:

Output
https://dav.r4ven.me/dashboard
Click to expand and view more

Enter ADMIN_LOGIN and ADMIN_PASSWORD from .env:

Open the users section (Users and Resources) and create a regular account:

Enter the following:

Creating a user also creates a calendar and an address book for them: Default Calendar and Default Address Book:

In the calendars section, hover over the i symbol to display the calendar’s full web address:

We will need this address when configuring clients.

Finally, open /dashboard/diagnostics and verify that migrations have run, directories are writable, and DAV endpoints are enabled:

About SMTP Configuration

Davis uses email for just one purpose: sending invitations to calendar event participants. For a personal calendar without invitations, leave MAILER_DSN=null://null and skip this section.

If you need invitations, open .env and change two variables:

BASH
vim /etc/compose/davis/.env
Click to expand and view more
INI
MAILER_DSN=smtp://no-reply%40r4ven.me:PASSWORD@smtp.r4ven.me:587
INVITE_FROM_ADDRESS=no-reply@r4ven.me
Click to expand and view more

The DSN uses the standard Symfony Mailer format: smtp://логин:пароль@сервер:порт. For implicit TLS on port 465, use the smtps:// scheme. On 587, the connection usually upgrades automatically through STARTTLS.

You can encode the password directly in the console:

BASH
python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1], safe=""))' 'my:p@ss/word'
Click to expand and view more
Output
my%3Ap%40ss%2Fword
Click to expand and view more

Your SMTP server must allow the INVITE_FROM_ADDRESS address as a sender; otherwise, it will reject the messages. If you leave the variable empty, Davis will not send invitations and will log a warning.

Apply the changes. Recreate the container so that it reads .env again:

BASH
cd /etc/compose/davis

docker compose up -d --force-recreate davis
Click to expand and view more

Check delivery with a test email, without creating an event:

BASH
docker compose exec davis php bin/console davis:mail:test kar-kar@r4ven.me
Click to expand and view more

The /dashboard/diagnostics page also shows the transport, but only the scheme and host, without credentials. It also shows how many users have no email address.

Automatic Startup with Systemd

The containers in our compose.yaml already have restart: always and restart: unless-stopped, so they start with Docker after a server reboot. But I prefer managing the entire stack consistently, like a regular service. This gives it a clear status, a dependency on docker.service, and systemctl start|stop|restart commands. We will create a simple unit for this.

Create the file:

BASH
vim /etc/compose/davis/davis.service
Click to expand and view more

Add the following:

INI
[Unit]
Description=Davis (Docker Compose)
Requires=docker.service
After=docker.service network-online.target
Wants=network-online.target

[Service]
Type=simple
WorkingDirectory=/etc/compose/davis
ExecStart=/usr/bin/docker compose up --no-color --remove-orphans
ExecStop=/usr/bin/docker compose down
Restart=on-failure
RestartSec=10
TimeoutStopSec=120

[Install]
WantedBy=multi-user.target
Click to expand and view more

Here, docker compose up runs without -d, so it stays in the foreground as the service’s main process. Thus, systemd sees the stack’s actual state, and all container output goes to the journal, journald. The --no-color flag removes ANSI codes so that journalctl does not show non-printable characters. Check the binary path with command -v docker, just in case; on Debian, it is /usr/bin/docker.

Create a symlink to our service file in the unit directory, reload systemd, enable automatic startup, and start it immediately:

BASH
ln -sv /etc/compose/davis/davis.service /etc/systemd/system/davis.service

systemctl daemon-reload

systemctl enable --now davis.service
Click to expand and view more

If you already started the stack manually with up -d, that is fine. Compose simply attaches to the running containers and starts showing their logs. Check the status and journal:

BASH
systemctl status davis.service

journalctl -fu davis.service
Click to expand and view more

You can now manage the stack like this:

BASH
systemctl restart davis.service

systemctl stop davis.service
Click to expand and view more

My final check is a server reboot. Afterward, I verify that the service and containers start automatically:

BASH
reboot

# after boot
systemctl is-active davis.service

docker compose -f /etc/compose/davis/compose.yaml ps
Click to expand and view more

Connecting WebDAV in Linux Mint

In Linux Mint, the Nemo file manager can connect to WebDAV without additional software.

Open Nemo and select Файл -> Подключиться к серверу from the menu:

And enter the parameters:

After connecting, the resource appears in Nemo’s sidebar. You can create directories, copy files, and open documents almost as you would on a regular filesystem.

If you prefer entering the address manually, use a URI with the davs scheme:

Output
davs://dav.r4ven.me/dav/home/
Click to expand and view more

Check write access by creating a small test directory or file. Then verify through WebDAV that the object exists in the Davis volume, not just in the file manager’s local cache:

Connecting a Calendar in Linux Mint

Use Online Accounts to connect the Linux Mint calendar to Davis over WebDav. You can launch it from the main application menu or the calendar:

Select WebDav as the provider:

Enter the following:

In the address field, enter the URL we saw earlier in the user’s calendars section:

Click “Sign In”, select the items to synchronize, and close the window using the “X” in the upper-right corner:

A new “Default calendar”, or whatever you named it, will appear in the calendar list:

For testing, schedule an event whose synchronization we will check on Android later.

The main point: select the correct calendar when editing the event. In my example, it is “Default Calendar”:

Connecting WebDav on Android with DAVx⁵

DAVx⁵ is an open-source Android application available from F-Droid and Google Play. It connects CalDAV and CardDAV to Android’s system stores. Regular calendar applications can then access the calendars, and the system contacts application can access the contacts. The application also connects WebDav storage as mount points for file access:

Install DAVx⁵ using your preferred method, grant the requested permissions, and create a new account.

Select login with a URL and enter:

Davis supports well-known discovery, so you can also try the root URL:

Output
https://dav.r4ven.me/
Click to expand and view more

However, the explicit /dav/ address is easier to troubleshoot, and the DAVx⁵ compatibility page for Davis recommends it.

After service discovery, set the account name. If you plan to use CalDAV invitations, it is better to use the user’s email address. Then open the account and select the calendars and address books to synchronize:

Run a manual synchronization. In Android’s system calendar, create an event specifically in the DAVx⁵ calendar. In the contacts application, create a contact in the DAVx⁵ address book. DAVx⁵ does not synchronize local phone collections or Google account collections.

Check the phone’s standard calendar (Samsung and OneUI in my case):

The event we added in the Linux Mint calendar is there 😌.

To access regular WebDAV files in DAVx⁵, configure a mount point in the application’s left sidebar:

The mount point will appear in Android’s standard file viewer:

Backup and Updates

In our configuration, the davis_data and davis_webdav volumes in /var/lib/docker/volumes/ store everything important.

Before updating Davis, stop write access:

BASH
systemctl stop davis
Click to expand and view more

And back up both volumes (/var/lib/docker/volumes/davis_data and /var/lib/docker/volumes/davis_webdav), for example, using my Bash script:

After the backup, change the ghcr.io/tchapi/davis-standalone:5.6.3 image tag in compose.yaml to the required version. Download the image and start the service again:

BASH
docker compose -f /etc/compose/davis/compose.yaml pull davis

systemctl start davis

journalctl -fu davis
Click to expand and view more

The startup command in compose.yaml will automatically apply database migrations.

Troubleshooting

DAVx⁵ Cannot Find Calendars and Contacts

Check the main endpoint and well-known redirects:

BASH
curl -I https://dav.r4ven.me/dav/

curl -I https://dav.r4ven.me/.well-known/caldav

curl -I https://dav.r4ven.me/.well-known/carddav
Click to expand and view more

Verify that the davis and davis-caddy containers share a network and Caddy forwards requests to davis:9000. In DAVx⁵, first use the explicit base URL https://dav.r4ven.me/dav/.

Error 401 When Connecting

The logs may show a message about a missing authorization header:

Output
401 - No 'Authorization: Basic' header found. Login was needed
Click to expand and view more

Such a request does not necessarily indicate a problem. Browsers and DAV clients may first connect without credentials, receive 401, and then retry with authentication. If login still fails, check the regular DAV user’s username and password.

You can view the application logs like this:

BASH
docker exec -it davis tail /var/www/davis/var/log/prod.log
Click to expand and view more

WebDAV Opens but Rejects Writes

Check that you are using the user’s personal path:

Output
https://dav.r4ven.me/dav/home/ivan/
Click to expand and view more

Also check permissions inside the container:

BASH
docker exec -it davis ls -ld /webdav /webdav/homes /webdav/homes/ivan
Click to expand and view more

During normal startup, the command in compose.yaml assigns ownership of /webdav to www-data.

If Davis generates links with an incorrect scheme or domain, check this setting:

INI
SYMFONY_TRUSTED_PROXIES=REMOTE_ADDR
Click to expand and view more

Caddy automatically forwards the standard X-Forwarded-Proto and X-Forwarded-Host headers. Also verify that Caddyfile specifies the davis:9000 upstream and both containers connect to the davis network.

Afterword

WebDav provides a general-purpose way to access remote storage over the standard HTTP protocol. It is not the fastest protocol, but it is widespread. Throughout my work with web technologies, I have always used WebDav for various tasks.

Davis was a pleasant discovery for cases where a full cloud service is unnecessary. It lets me keep standard calendars, contacts, and WebDAV on my own server. Two containers, SQLite, and automatic HTTPS make a great solution for a small home infrastructure 😌.

The main thing now is to remember backups. Especially given recent events. Synchronization is convenient, but that does not make it a backup.

Thanks for reading! And take care of your data.

References

Comments

Copyright Notice

Author: Ivan Cherniy

Link: https://r4ven.me/en/storage/davis-ustanovka-i-nastroyka-webdav-caldav-carddav-servera/

License: CC BY-NC-SA 4.0

Blog materials may be used with attribution to the author and source, for non-commercial purposes, and under the same license.

Start searching

Enter keywords to search articles

↑↓
↵
ESC
⌘K Shortcut