I am utterly lost on how to use my NVMe SSD for my Postgres data, i.e. /var/lib/postgres/data
, in my single-container application on my Jetson Xavier NX SD Card devkit. I have BalenaOS and the containers running from the SD Card, but I would like the database to be persisted and stored on the NVMe. I have the following Dockerfile:
# FROM balenalib/jetson-xavier-nx-devkit-ubuntu
# FROM balenalib/jetson-xavier-nx-devkit-alpine
FROM timescale/timescaledb:latest-pg13
ENV POSTGRES_PASSWORD "password123"
ENV TIMESCALEDB_TELEMETRY "off"
This successfully builds an image with Postgres/Timescale, but the data will be stored at /var/lib/postgres/data
on the SD card, or eMMC depending on the Jetson Xavier NX devkit and device provisioning. I SSH-ed into the running container, and I manually mounted the NVMe SSD, where I have formatted the NVMe with the ext4
format and given it the nvme
label. At least the NVMe is recognized by the host and I can manually access it from a container.
$ balena push <UUID>.local
$ balena ssh <UUID>.local main
bash-5.1# blkid | grep 'LABEL="nvme"'
/dev/nvme0n1p1: LABEL="nvme" UUID="ed03fc8a-ce3f-4a2c-a939-8c12b520e271" TYPE="ext4"
bash-5.1# mkdir -p /mnt/nvme
bash-5.1# mount -t ext4 -o rw /dev/nvme0n1p1 /mnt/nvme
bash-5.1# ls /mnt/nvme
lost+found
The TimescaleDB source image contains an Alpine distribution, and I have done the above with the balenalib/jetson-xavier-nx-devkit-alpine
and balenalib/jetson-xavier-nx-devkit-ubuntu
base images, too. I recognize that the Balena base images do some additional work for udev and device/hardware access, but regardless of the base/parent image used, I am still having problems.
Side note, I was initially confused by the Mounting external storage media documentation because it is for the non-Alpine base images despite the example project using an Alpine base image. There is no -L
option for the mount
command in the Alpine base images. Thus, the blkid | grep 'LABEL="nvme"'
command is used to get the device name instead of the recommended label-based mounting command.
In my naive understanding, I converted the commands from the SSH session to manually mount the drive to the Dockerfile:
# DO NOT FOLLOW THIS DOCKERFILE. IT WILL NOT WORK.
# FROM balenalib/jetson-xavier-nx-devkit-ubuntu
# FROM balenalib/jetson-xavier-nx-devkit-alpine
FROM timescale/timescaledb:latest-pg13
RUN mkdir -p /mnt/nvme
RUN mount -t ext4 -o rw /dev/nvme0n1p1 /mnt/nvme
RUN mkdir -p /mnt/nvme/postgres
ENV PGDATA "/mnt/nvme/postgres/data"
ENV POSTGRES_PASSWORD "password123"
ENV TIMESCALEDB_TELEMETRY "off"
This does not work, regardless of the base image used. I get a mount: permission denied (are you root?)
error. I think the error is caused by the device, /dev/nvme0n1p1
, not being available during the build. The Dockerfile is a recipe for building an image, not a “startup” script.
So then, there must be some way to mount an “external” drive, but just once when the container is started, not dynamically while the container is running. I don’t think I need udev for this, as the documentation seems to indicate that udev is only needed for dynamically mounting and unmounting USB drives and/or additional SD cards. Plus, the dynamic udev detection would be started after the container has started. I think I need the NVMe to be mounted before Postgres is started. Taking inspiration from the example storage project, I created the following script:
#!/env/bin bash
mkdir -p /mnt/nvme
mount -t ext4 -o rw /dev/nvme0n1p1 /mnt/nvme
mkdir -p /mnt/nvme/postgres
I could not figure out how to run this script at boot of the container without overriding the entry point for the Postgres/Timescale parent image. After a lot of reading, experimenting, and cursing, I thought I found a solution by using a custom fstab
file within the container that would automatically mount the NVMe to the /mnt/nvme
mount point on boot without having to execute the above script. I used the following fstab file and modified Dockerfile:
LABEL=nvme /mnt/nvme ext4 defaults 0 0
# DO NOT FOLLOW THIS DOCKERFILE. IT WILL NOT WORK.
# FROM balenalib/jetson-xavier-nx-devkit-ubuntu
# FROM balenalib/jetson-xavier-nx-devkit-alpine
FROM timescale/timescaledb:latest-pg13
RUN mkdir -p /mnt/nvme
COPY ./fstab /etc/fstab
ENV PGDATA "/mnt/nvme/postgres/data"
ENV POSTGRES_PASSWORD "password123"
ENV TIMESCALEDB_TELEMETRY "off"
This also did not work. If I SSH log into the host and mount the NVMe manually to /tmp/nvme
, then the contents of /tmp/nvme
is just a “lost+found” folder with no postgres/data
folder. However, if I SSH log into the container, the /mnt/nvme/postgres/data
location does exist, and it contains Postgres-related files and folders, but if I rebuild and restart the container, then any data within the database is lost. This might not be working because the fstab file is not actually mounting the NVMe but mounting the folders like a symbolic link still on the SD card.
I tried playing around with a docker-compose.yml
file and named volumes, as this appears to be the appropriate way to persist data. All of the StackOverflow Q&As and Docker-related documentation I can find through various Internet searches suggest mounting the device, mount -L nvme /mnt/nvme
, first, then using volumes within a docker-compose file. I cannot figure out how to have the NVMe mounted automatically by BalenaOS and/or executing the NVMe mount by the host. I tried various versions of the following docker-compose file to no resolution:
# DOES NOT WORK. DO NOT USE.
version: "2"
services:
database:
build: .
volumes:
- pgdata:/var/lib/postgres/data
volumes:
pgdata:
driver: local
driver_opts:
device: /dev/nvme0n1p1
I recognize that named volumes are stored in /var/lib/docker/volumes
. The named volume would be on the SD Card or eMMC, but I thought there would be some way to have this specific volume on the NVMe. I found some StackOverflow Q&As that indicated a symbolic link could be created, but this is assuming CLI access and running containers with docker on the host system, not with BalenaOS. This also feels like a hack for some reason.
I feel like this should be relatively straight-forward, but I am new to all of this (Docker, Balena, and containers) and there must be something I am missing. I am hoping the Balena community can help me out.