Deploying Metabase as a Quadlet: A Rootless Podman Journey.

Not many images for "quadlet podman metabase almalinux" but Stable Diffusion gave it its best

Recently I have been getting into Podman as a great (rootless) Docker alternative and its neat integration into Redhat based Linux distributions, rekindling my decade old love affair with Red Hat Linux. Being asked to deploy Metabase (Open-source Edition) as an internal service at my place of work, I decided to give the somewhat new Quadlets a try in deploying the service, instead of the older, deprecated way of asking Podman to generate systemd unit files.

I’ve been using Alma Linux 9.4, but this will work equally in Rocky Linux or Redhat Enterprise Linux of the same version.

Architecture

We are going to create a dedicated Metabase user on the host, running their official Docker image with a Postgresql sidecar (application database) for the application and a secondary Postgresql sidecar (source database) for the source data for generating reports. The application database will be backed up nightly, the source database will be restored daily from a production backup.

Requirements

We need mainly Podman, but as my base installation was relatively minimal, I installed a few extra bits as well.

sudo dnf install podman tar unzip buildah postgresql htop

Metabase

First I created a dedicated user for Metabase, calling it metabase. This is not strictly required, but I like to deploy larger applications in their own little enclave. Furthermore, we will enable lingering for our newly created user, so that its systemd services will restart after a reboot.

After creating the user we will switch to it via sudo -i -u metabase

# Create metabase user
adduser metabase

# Enable linger for user (this ensures that services survive a reboot)
sudo loginctl enable-linger metabase

# Switch to the newly created user
sudo -i -u metabase

For some reason my Alma Linux install didn’t set XDG_RUNTIME_DIR which is required to start systemd services as a user, so I had to create a script in .bashrc.d:

# Enable systemd user integration environment variable
mkdir -p ~/.bashrc.d
echo "export XDG_RUNTIME_DIR=/run/user/$(id -u)" > ~/.bashrc.d/systemd
source ~/.bashrc.d/systemd

Now we create the local directories for the database volumes and the local .config directory for our Quadlet files:

# Create database volume containers
mkdir -vp ~/volumes/{metabase-db,metabase-source-db}

# Create Quadlet directory (systemd container files)
mkdir -p ~/.config/containers/systemd

Then we place the following four files into ~/.config/containers/systemd.

Please note: Amend the value of MB_DB_PASS and POSTGRES_PASSWORD to suit your needs. You could use a Podman secret or just add a reasonably complicated password in the file itself. We will interact via podman exec with the database, so will not require these passwords again. MB_DB_PASS and the value of POSTGRES_PASSWORD in the metabase-db.container have to match. You will need the POSTGRES_PASSWORD of the metabase-source-db.container when setting up a datasource in Metabase itself.

metabase.network

The simplest of them all. It will create the metabase network that the other three containers will use.

# ~/.config/containers/systemd/metabase.network
[Network]

metabase-app.container

# ~/.config/containers/systemd/metabase-app.container
[Container]
Image=docker.io/metabase/metabase:latest
AutoUpdate=registry
PublishPort=3000:3000
Network=metabase.network
Environment=MB_DB_TYPE=postgres
Environment=MB_DB_DBNAME=metabase
Environment=MB_DB_USER=metabase
Environment=MB_DB_PASS=CHANGEME
Environment=MB_DB_HOST=metabase-db
Environment=MB_DB_PORT=5432

[Unit]
Requires=metabase-db.service
After=metabase-db.service

[Service]
Restart=always

[Install]
WantedBy=default.target

metabase-db.container

The main database for Metabase to store its own state. Remember to change CHANGEME!

# ~/.config/containers/systemd/metabase-db.container
[Container]
Image=docker.io/library/postgres:16
AutoUpdate=registry
PublishPort=5432:5432
Volume=%h/volumes/metabase-db:/var/lib/postgresql/data:Z
Network=metabase.network
HostName=metabase-db
Environment=POSTGRES_USER=metabase
Environment=POSTGRES_PASSWORD=CHANGEME

[Service]
Restart=always

[Install]
WantedBy=default.target

metabase-source-db.container

The database we set as a datasource for Metabase. As we are running Postgres in production, we could simply replicate the metabase-db.container setup. Please change example to something meaningful, e.g. your company name, project, etc. Also remember to change CHANGEME!

# ~/.config/containers/systemd/metabase-source-db.container
[Container]
Image=docker.io/library/postgres:16
AutoUpdate=registry
PublishPort=5433:5432
Volume=%h/volumes/metabase-source-db:/var/lib/postgresql/data:Z
Network=metabase.network
HostName=metabase-source-db
Environment=POSTGRES_USER=example
Environment=POSTGRES_PASSWORD=CHANGEME

[Service]
Restart=always

[Install]
WantedBy=default.target

Starting Metabase and its services

Now we can reload the Metabase user’s systemd daemon and start our containers:

# Reload systemd user daemon, this links and compiles the above quadlet files
systemctl --user daemon-reload

# Start containers
systemctl --user start metabase-db.container
systemctl --user start metabase-source-db
systemctl --user start metabase-app

If all went well we can now continue with the Metabase application setup.

Initial Metabase setup

Log out of your server and SSH into it again with port forwarding:

# Running htop so that the connection remains open, you can also use the `ServerAliveInterval` SSH option
ssh -L 3000:localhost:3000 [email protected] -t htop

Navigate to http://localhost:3000 for the Metabase admin user creation and initial setup. As much as the initial Metabase setup isn’t part of this guide, but we ended up hosting it through a Cloudflare tunnel and using Google as our single-sign-on provider.

Backup and restore

I’m only listing this as a help on how to use zcat and podman exec to restore a source database backup.

First grab a database backup and place it in /home/metabase/latest.sql.gz.

# Restore source data backup, using podman exec avoids having to remember the database password
zcat latest.sql.gz | podman exec -i systemd-metabase-source-db psql --username=example -h localhost example

Again, listing this as a help on how to use podman exec to create a backup of Metabase’s database.

BACKUP_FILE="metabase_$(date +'%Y-%m-%d-%H%M%S').sql"
podman exec -it systemd-metabase-db pg_dump -U metabase -h localhost > "$BACKUP_FILE"

Automatic updates of Metabase container

As we are targeting the latest tag for Metabase and their frequent release cycle, we can enable automatic updates of their Docker container via Podman like so:

systemctl --user enable podman-auto-update.{service,timer}
systemctl --user start podman-auto-update.timer

Even if this could result in a broken install at some point, using a nightly backup, one could always roll back to a last known good state. We have been running the above setup for a while now without a hitch but your mileage may vary.