Podman quadlets for Seafile V13 including notification server, redis, maria-db, onlyoffice, caddy and rclone

Hello everyone,
I finally managed to migrate my entire server to rootless podman quadlets.

Advantages in my opinion:

  1. No rootful docker containers
  2. Even regular systemd services like rclone in my example can be made dependent on a container, so they start after each other in the right order
  3. I have a separate config file for each container, so I can e.g. use one maria-DB container for different container apps without having to start them together in one stack, thanks to systemd quadlets, they start in the right order.
  4. Podman has auto-update inbuilt, no need for watchtower or similar addon aps (I have a small script in place that checks for updates once a week and notifies me by email, so I can check if some updates contain breaking changes that need manual interaction, afterwards I push the updates with ā€œpodman auto-updateā€.

Drawback:

  1. Withouth enabling the podman socket (which is not recommended), caddy docker proxy does not work, so I use a regular Caddyfile.
  2. There is no dedicated quadlet editor for the web like portainer, cockpit provides a podman plugin, but without proper quadlet support so far.

Since this was quite some work, I would like to share this with other forum members in case they are interested.
Quadlet files (.container &.network) go to ~/.config/containers/systemd, I keep .env files in the same folder.
.service files for rclone goes to ~/.config/systemd/user

Let’s start with the seafile.env

TIME_ZONE=your timezone
SEAFILE_SERVER_HOSTNAME=https://my-seafiledomain.com
SEAFILE_SERVER_PROTOCOL=https
JWT_PRIVATE_KEY=your jwt key

ENABLE_SEADOC=false
##I don’t use seadoc at all

SEAFILE_MYSQL_DB_HOST=maria-db
SEAFILE_MYSQL_DB_PORT=3306
SEAFILE_MYSQL_DB_USER=seafile
SEAFILE_MYSQL_DB_PASSWORD=your seafilepassword

for fresh install you also need to use the root db password variable, but not for migration from docker

SEAFILE_MYSQL_DB_CCNET_DB_NAME=ccnet_db
SEAFILE_MYSQL_DB_SEAFILE_DB_NAME=seafile_db
SEAFILE_MYSQL_DB_SEAHUB_DB_NAME=seahub_db

CACHE_PROVIDER=redis

REDIS_HOST=seafile-redis
REDIS_PORT=6379

db.network (to create a custom podman network)

[Unit]
Description=Database Network
After=network-online.target

[Network]
NetworkName=db
Subnet=10.90.0.0/24
Gateway=10.90.0.1

[Install]
WantedBy=default.target

seafile-server.container

[Unit]
Description=Seafile Server
Requires=maria-db.service seafile-redis.service seafile-notifications.service oods.service
After=maria-db.service seafile-redis.service seafile-notifications.service oods.service

[Container]
ContainerName=seafile-server
EnvironmentFile=seafile.env
Image=docker.io/seafileltd/seafile-mc:13.0-latest
Label=io.containers.autoupdate=registry

Network=db
PublishPort=3001:80
Volume=/Media/Seafile:/shared

[Service]
Restart=on-failure

[Install]
WantedBy=default.target

seafile-notifications.container

[Unit]
Description=Seafile Notification Server
After=network.target
Wants=network.target

[Container]
Image=docker.io/seafileltd/notification-server:13.0-latest
ContainerName=seafile-notifications
Label=io.containers.autoupdate=registry

Network=db
EnvironmentFile=seafile.env

Volume=/Media/Seafile/seafile/logs:/shared/seafile/logs

PublishPort=8083:8083

[Service]
Restart=on-failure

[Install]
WantedBy=default.target

seafile-redis.container

[Unit]
Description=Seafile Redis Cache Server

[Container]
ContainerName=seafile-redis
Image=docker.io/redis:latest
Label=io.containers.autoupdate=registry
Network=db
HealthCmd=redis-cli ping || exit 1

[Service]
Restart=on-failure
Notify=healthy

[Install]
WantedBy=default.target

maria-db.container

[Unit]
Description=MariaDB container
After=network.target

[Container]
ContainerName=maria-db
Image=docker.io/mariadb:lts
Label=io.containers.autoupdate=registry
Network=db

HealthCmd=/usr/bin/mariadb-admin ping -h 127.0.0.1 -uroot -pyourrootpass --silent
HealthInterval=10s
HealthRetries=5
HealthTimeout=5s

Volume=/Databases/MariaDB:/var/lib/mysql

Environment=MARIADB_ROOT_PASSWORD=yourrootpass
Environment=MYSQL_INITDB_SKIP_TZINFO=1
Environment=MYSQL_LOG_CONSOLE=true
Environment=MARIADB_AUTO_UPGRADE=1

[Service]
Restart=on-failure
Notify=healthy

[Install]
WantedBy=default.target

oods.container

[Unit]
Description=OnlyOffice Document Server

[Container]
ContainerName=oods
Environment=JWT_ENABLED=true
Environment=JWT_SECRET=jwt secret
Image=docker.io/onlyoffice/documentserver:latest
Label=io.containers.autoupdate=registry

PublishPort=8086:80
[Service]
Restart=on-failure

[Install]
WantedBy=default.target

caddy.container

[Unit]
Description=Caddy container
After=network.target

[Container]
ContainerName=caddy
Image=docker.io/caddy:latest
Label=io.containers.autoupdate=registry
Network=host
Environment=XDG_DATA_HOME=/data

Volume=/Storage/Caddy/Caddyfile:/etc/caddy/Caddyfile
Volume=/Storage/Caddy/data:/data/caddy
[Service]
Restart=on-failure

[Install]
WantedBy=default.target

rclone.service (similar to seaf-fuse, but with full write access, not read only, optional)

[Unit]
Description=Rclone mount for Seafile
After=seafile-server.service
Requires=seafile-server.service

[Service]
Type=simple
ExecStart=/usr/bin/rclone mount Seafile: /RClone
–vfs-cache-mode full
–dir-cache-time 72h
–poll-interval 15s
ExecStop=/bin/fusermount -u /path/to/mountpoint
Restart=on-failure
RestartSec=5

[Install]
WantedBy=default.target

Rclone needs to be configured once with an inbuilt assistance, this will create a config file within ~/.config and this service file will automatically start this (or you can comment out the line wantedby= and start it manuall).

Caddyfile

my-seafiledomain.com {
reverse_proxy :3001 {
}
handle_path /notification/* {
reverse_proxy :8083
}
handle_path /office/* {
reverse_proxy :8086 {
header_up X-Forwarded-Host {host}/office
}
}
}

Thanks a lot for daniel.pan in helping me to fix this caddy config for running onlyoffice in a subfolder of the seafile-domain (no additional port or separate sub-domain required).

To make this work, you also need to update your seahub_settings.py as followed:

ONLYOFFICE_APIJS_URL = ā€˜https://my-seafiledomain.com/office/web-apps/apps/api/documents/api.js’

Final remarks, I’ve added the following lines to my .bashrc to facilitate podman container handling (restart, stop, status)

##Custom Podman functions for bash
alias sudr=ā€˜systemctl --user daemon-reload’
_pcac() {
local cur container_services
COMPREPLY=()
cur=ā€œ${COMP_WORDS[COMP_CWORD]}ā€

# List all user services (active + inactive), match those derived from Quadlet (.container)
container_services=$(systemctl --user list-units --type=service --all --no-legend \
    | awk '{print $1}' \
    | sed 's/\.service$//')

COMPREPLY=( $(compgen -W "${container_services}" -- "${cur}") )
return 0

}
complete -F _pcac pcr
complete -F _pcac pcs
complete -F _pcac pcst

pcr() {
systemctl --user restart ā€œ$1.serviceā€
}
pcs() {
systemctl --user stop ā€œ$1.serviceā€
}
pcst() {
systemctl --user status ā€œ$1.serviceā€
}
##End of podman functions

So sudr is used to reload container quadlet config after changes were made to the files.
pcr seafile-server restarts the container (and all dependencies), pcs seafile-server stops the server and pcst seafile-server provides status information. For more detailed and realtime info on a container you can use e.g. podman logs -f seafile-server.

It is possible to harden this even further by using podman secrets for the passwords, but for now I guess this is enough. Feel free to comment on this setup or ask questions if you are interested in trying this.

Good luck!
Regards, Ruediger

P.S. Of note, I don’t use podman volumes for permanent data, but I prefer volume mounts directly to my zfs datasets to facilitate backups and direct access to all files from the host.

2 Likes