Ulaknode with encrypted storage

This guide explains how to run Ulaknode on a LUKS-encrypted volume so that mail data at rest is protected by a key that only you hold. The encrypted volume is managed entirely on the host — no changes are required inside the Ulaknode container.

Who this is for: organisations in regulated industries (legal, healthcare, finance) or any deployment where the hosting provider or vendor must not be able to read mail data at rest. If you don't have a specific compliance or privacy requirement, the standard self-hosted setup is sufficient.

How it works

You create a LUKS-encrypted block device or image file on the host and mount it to a directory (e.g. /srv/ulaknode-data). The Ulaknode Docker volumes point to that directory. While the volume is mounted, the container reads and writes normally. When the volume is closed, all data on disk is encrypted and unreadable without the key.

The key never leaves your hands. If you lose it, the data is irrecoverable — by you or anyone else.

Requirements

  • A working Ulaknode self-hosted deployment (see the Self-Hosted guide)
  • cryptsetup installed on the host (apt install cryptsetup on Debian/Ubuntu)
  • A spare block device or free disk space for an image file
  • Root access on the host

1. Create the encrypted volume

Choose either a dedicated block device (e.g. a second disk) or a file-backed image. The file-backed approach works on any server without an extra disk.

Option A — file-backed image (no extra disk required):

# Create a 50 GB image file (adjust size to your needs)
fallocate -l 50G /srv/ulaknode.img

# Initialise LUKS — you will be prompted for a passphrase
cryptsetup luksFormat /srv/ulaknode.img

Option B — dedicated block device:

# Replace /dev/sdb with your actual device — double-check before proceeding
cryptsetup luksFormat /dev/sdb

Warning: luksFormat destroys all existing data on the target. Verify the device path carefully.

2. Open the volume and create a filesystem

Open the LUKS container (you will be asked for the passphrase you set above):

# For the file-backed image:
cryptsetup luksOpen /srv/ulaknode.img ulaknode-data

# For a block device:
cryptsetup luksOpen /dev/sdb ulaknode-data

This creates a decrypted device at /dev/mapper/ulaknode-data. Format it (first time only):

mkfs.ext4 /dev/mapper/ulaknode-data

3. Mount the volume

mkdir -p /srv/ulaknode-data
mount /dev/mapper/ulaknode-data /srv/ulaknode-data

4. Point Docker volumes to the encrypted mount

In your docker-compose.yml, replace the named volumes with bind mounts into /srv/ulaknode-data. Edit the volumes: block of the service and remove the named-volume declarations at the bottom:

services:
  ulaknode:
    ...
    volumes:
      - /srv/ulaknode-data/mail:/var/mail
      - /srv/ulaknode-data/logs:/var/log
      - /srv/ulaknode-data/clamav:/var/lib/clamav
      - /srv/ulaknode-data/postfix:/etc/postfix
      - /srv/ulaknode-data/dovecot:/etc/dovecot
      - /srv/ulaknode-data/rspamd:/etc/rspamd
      - /srv/ulaknode-data/certs:/etc/ssl/mail
      - /etc/letsencrypt/live/mail.yourdomain.com/fullchain.pem:/run/certs/fullchain.pem:ro
      - /etc/letsencrypt/live/mail.yourdomain.com/privkey.pem:/run/certs/privkey.pem:ro

# Remove the top-level "volumes:" section entirely

Note: The two /run/certs/ bind mounts are read-only and sourced from Let's Encrypt on the host — they do not need to be on the encrypted volume.

Create the directories before starting the container:

mkdir -p /srv/ulaknode-data/{mail,logs,clamav,postfix,dovecot,rspamd,certs}

Then start Ulaknode as usual:

docker compose up -d

5. Reboot procedure

LUKS volumes do not auto-mount after a reboot — this is intentional. Each time the host restarts you must unlock and mount the volume before starting the container:

# 1. Open the LUKS volume (enter passphrase when prompted)
cryptsetup luksOpen /srv/ulaknode.img ulaknode-data

# 2. Mount it
mount /dev/mapper/ulaknode-data /srv/ulaknode-data

# 3. Start the container
docker compose up -d

Note: Automating this (e.g. via a systemd unit with a stored keyfile) weakens the security guarantee, because the key is then accessible to the OS at rest. For high-security deployments, manual unlock on boot is the recommended approach.

6. Closing the volume

To lock the volume (e.g. before shutting down):

docker compose down
umount /srv/ulaknode-data
cryptsetup luksClose ulaknode-data

Once closed, all data on the volume is encrypted and unreadable without the passphrase.

Key management

  • Back up your passphrase. Store it in a password manager or physical safe — offline, off the server. If you lose it, the data is permanently unrecoverable.
  • LUKS key slots — you can add a second passphrase (e.g. for a second administrator) without replacing the first: cryptsetup luksAddKey /srv/ulaknode.img
  • Passphrase rotation — remove an old passphrase after adding a new one: cryptsetup luksRemoveKey /srv/ulaknode.img
  • Header backup — back up the LUKS header in case of disk corruption: cryptsetup luksHeaderBackup /srv/ulaknode.img --header-backup-file ulaknode-luks-header.bak. Store this backup separately from the passphrase.

Support considerations

When the volume is open and the container is running, Ulaknode support can assist with configuration, service, and log issues in the same way as a standard deployment. Support staff cannot access data at rest when the volume is closed, and cannot access your passphrase at any time. For data-related diagnostics, you will need to share relevant log excerpts yourself.

Troubleshooting

  • Container fails to start after reboot: the volume is not mounted — run the reboot procedure in step 5 before docker compose up
  • Device busy on umount: ensure the container is fully stopped (docker compose down) before unmounting
  • Wrong passphrase error: LUKS has no password hint — verify you are using the correct passphrase from your key store
  • No space left on device inside the container: the encrypted image file is full — either extend it (cryptsetup resize after growing the image) or free disk space