Containerized PostgreSQL with rootless Podman
This post is about running rootless Podman containers with PostgreSQL as a central example 📦️
The post serves as an optional preparation for the next post about interacting with databases with SQLx in Rust 🦀
Landscape mode recommended on mobile devices
Contents
Disclaimer
To restrict the scope of the post, I will assume that you know what a (rootless) Linux container is.
I will not cover Docker since there are many Docker tutorials in the internet. Just search for docker postgres
in your favorite search engine 🔎
Nevertheless, almost all of the presented Podman commands work with Docker by replacing the binary podman
with docker
at the beginning of the command.
This post is not about SQL. Therefore, the details of SQL statements will not be explained.
Installation
Before starting with Podman containers, follow the official installation guide to install Podman on your system.
Note
Podman is preinstalled on Fedora Linux 😉
PostgreSQL container
To pull and run the PostgreSQL container, run the following command in your favorite shell:
podman run \
-it \
--rm \
--name test-db \
-p 5432:5432 \
-e POSTGRES_PASSWORD=CHANGE_ME \
docker.io/library/postgres:15
Let's break it into parts:
podman run
runs a container from the image specified after the options. If the image does not already exist, it will be pulled first (which is why the first run takes time with downloading).-it
runs the container with the ability to interact with it.--rm
removes the container after exiting (read further for persistent data storage). The image is not removed.--name test-db
specifies the nametest-db
of the container to be able to interact with it later while it is running.-p 5432:5432
publishes the container's port5432
to the host system's port5432
. You could use something like-p 5555:5432
so that you can connect to the database on port5555
, but normally you can just keep it5432
. You can use the option-p
more than once.-e POSTGRES_PASSWORD=CHANGE_ME
specifies the environment variablePOSTGRES_PASSWORD
inside the container. This environment variable sets the password of the default database userpostgres
. You can use this option-e
more than once.docker.io/library/postgres:15
is the PostgreSQL container image from Docker Hub.15
is a tag that fetches the image with the latest PostgreSQL version15.x.x
. If you want to use the version14
, you can just replace the tag.
Note
The command doesn't need sudo
in contrast to Docker!
This is why we are talking about rootless Podman containers which offer better security than rootful containers.
After running the command, you will see PostgreSQL starting. You can connect to the default database postgres
on localhost:5432
with the default user postgres
and the entered password.
⚠️ Warning ⚠️
To stop the container, press CTRL+C
. You will loose all the data in the database because we didn't configure persistent data storage yet!
Persistent data storage
To keep the data in the database even after stopping the container, we have to mount a volume.
To do so, create the directory data
on the host system and add the following option to the podman run
command: -v ./data:/var/lib/postgresql/data:Z
.
-v /src/path:/dest/path
is an option that mounts a volume so that the container can read and persistently write into /dest/path
which is accessible by the host system in /src/path
.
Note
You can use the option -v
more than once.
:Z
is a SELinux flag that is needed if your host system has SELinux enabled (like in Fedora). It tells the system that this volume will not be shared with other containers.
Note
If you do want to share a volume with other containers, you can use :z
(lowercase) instead, but this is not a good idea for a database system.
How did I know about using /var/lib/postgresql/data
as the destination directory?
Every container image has its own volumes documented in the image's documentation. For the used PostgreSQL image, the image's documentation can be found here.
Interacting with the database
Run the container again with a mounted volume from the section before.
Let's say we want to test that our data is now persistent. How can we interact with the database quickly?
To do so, open a second terminal tab and run the following command (while the container is still running):
podman exec -it test-db psql -U postgres
-it
enables interactivity as withpodman run
.test-db
is the name that we specified with the--name
option in thepodman run
command.psql -U postgres
is the command (with arguments) that we want to run inside the container.
After running this command, PostgreSQL will be waiting for our input:
psql (15.2 (Debian 15.2-1.pgdg110+1))
Type "help" for help.
postgres=#
Let's create a table called notes
with a self incrementing id
as the primary key and a text note
:
CREATE TABLE IF NOT EXISTS notes (id SERIAL PRIMARY KEY, note TEXT NOT NULL);
Now, verify that the table notes
was created by listing all relations with \dt
:
postgres=# \dt
List of relations
Schema | Name | Type | Owner
--------+-------+-------+----------
public | notes | table | postgres
Let's insert a row to our new table:
INSERT INTO notes (note) VALUES ('Testing containerized PostgreSQL');
Now, view all rows:
SELECT * FROM notes;
The output should be the following:
id | note
----+----------------------------------
1 | Testing containerized PostgreSQL
Our database works 🎉 But does it store the data persistently?
Press CTRL+D
to exit psql
. Go back to the first terminal tab and stop the container with CTRL+C
.
Now that the container is stopped, we want to verify that our data is stored persistently.
Run the podman run
command again with the volume option. Go to the second terminal tab and run the podman exec
command again and enter the following:
SELECT * FROM notes;
If you see your note "Testing containerized PostgreSQL", then your data was persistently stored 🎉
Running in the background
If you want to run a container in the background, replace -it
with -d
which stands for "detach". This way, you don't need to keep a terminal tab opened.
To verify that the container is running, run the command podman ps
. It's output should look like the following:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
74c9ea0ac068 docker.io/library/postgres:15 postgres 2 minutes ago Up 2 minutes 0.0.0.0:5432->5432/tcp test-db
To stop a container running in the background, run podman stop CONTAINER_NAME
.
If you want to see the logs of a container running in the background, run the command podman logs CONTAINER_NAME
.
A container running in the background will not be running after a reboot! To automatically start a container after a reboot, we need a systemd service.
Systemd service
⚠️ Warning ⚠️
podman generate systemd
is now deprecated!
You should use Quadlet instead.
I have a blog post about it: Quadlet: Running Podman containers under systemd
Automatically starting a container after a system reboot is especially important if you want to run the container on a server.
This can be achieved with a systemd service that Podman can generate for us.
Before generating a service file, we have to create a container with podman create
instead of podman run
:
podman create \
--name test-db \
-p 5432:5432 \
-e POSTGRES_PASSWORD=CHANGE_ME \
-v /home/USERNAME/volumes/test-db:/var/lib/postgresql/data:Z \
docker.io/library/postgres:15
podman create
creates a container without starting it.
The differences to our podman run
command are that we removed the options -it
and --rm
and used an absolute path for our volume's source path.
If you run podman ps
, then you should not find the container with the name test-db
because it is not running.
The option -a
shows all created containers (not only the ones running). The output of podman ps -a
should look like the following:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
889f7f2c1c23 docker.io/library/postgres:15 postgres 25 seconds ago Created 0.0.0.0:5432->5432/tcp test-db
Now, you can generate the systemd service file with the following command:
podman generate systemd test-db -fn --new
-f
creates a service file in the current directory instead of printing its content to stdout.-n
uses the name of the container in the file name instead of its ID.--new
creates a new container instead of starting one that is already created. Since we are using a volume for persistent data, this is fine and comparable to--rm
withpodman run
.
After running the command, you will find a file named container-test-db.service
in your current directory.
Move this service file to the user's configuration directory of systemd (create the directory if it doesn't already exist):
mv container-test-db.service ~/.config/systemd/user
Now, enable the service:
systemctl --user enable --now container-test-db.service
This service will start the container after every reboot.
--now
also starts the container now instead of waiting for the next reboot.
To see the status of the container service, run systemctl --user status container-test-db
.
Since we use user services for systemd, we have to enable the linger for our user to start the containers without the user being logged in:
loginctl enable-linger
⚠️ Warning ⚠️
Enabling the linger is required for the container to be automatically started after a server reboot!
To disable the automatic restart of the container later, run systemctl --user disable --now container-test-db
.
Configuration
Container images usually use environment variables for configuration.
This is also the case for the PostgreSQL
image.
The only required configuration variable is POSTGRES_PASSWORD
which we already used.
But there are other configuration variables that can be found in the documentation of the container image.
Closing words
I hope that you found this short introduction to rootless Podman helpful.
Now that we know how to run a PostgreSQL container, we can connect to such a database with SQLx in Rust. This will be the topic of the next post 😃