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.

3 Likes

Good approach, I did the same thing, even scoped to its own linux user.

Though you shouldn’t hardcode secrets into the quadlets. You can pass an env file instead, but that exactly is one of the drawbacks of this approach. You can’t load individual env vars from a file, you need to bring in the entire file, which makes isolation of secrets such a pain (which I never bothered with).

Frankly, in hindsight I think I’d just run the docker compose file with podman-compose though I haven’t tested it.

As far as I understand, you can use a separate feature, podman secrets, instead of placing passwords in the quadlet files or the env files.
I use dedicated env files if the app has a lot of environment variables, so I avoid writing Environment= a lot (lazy person) :slight_smile:
If there are only a few variables, I’ll keep them in the quadlet. Perhaps not very consistent, but reasonable.

You can for sure use podman-compose with compose files, but that’s not the recommended way for podman. Also you would loose the ability of podman to have dependencies across different compose stacks.

e.g. if you run seafile and sogo with the same maria-db container, you would have to stack all those containers into one compose stack to make sure that mariadb is up before both containers using the db start. There are some work-around for that, but they are actually all rather complicated and not really a clean solution, but a work-around. with quadlets, each container has it own config file (.container), and they can still depend on each other.

Very nice, if you use this together with zfs snapshots.
I created a batch file for performing my snapshots, that starts with

systemctl --user stop maria-db.service

This command will stop maria-db and all dependent quadlets at once.
Then it performs a zfs snapshot and afterwards it restarts the apps containers. Maria-DB will start automatically, as it is set as requirement inside the app quadlets.