PostgreSQL on Kamal: Deployment, Configuration, and Backups

It's been a few weeks since I started building txtcv, which is a simple career management platform (CV builder, cover letter generator, job application tracker) aimed mostly at software developers. Built using Django and some Htmx (where needed), it's deployed on a 2.50 €/month machine on a VPS hosting provider using Kamal.

When starting out, I consciously made the decision to not use any cloud hosting providers, mainly because I wanted a side-project to be fun to work on (and cloud hosting providers take the fun out of deployments if you're the kind of person who has fun when deploying applications), but also because you can get a lot more processing power on a VPS than on a cloud hosting provider for the same amount of money.

After looking around and experimenting with tools that would make this process easy, I settled on Kamal.

Kamal Deploy

Kamal, if you're not familiar, is a command-line tool that lets you easily deploy web applications to servers. Apart from deployments, it also handles tasks like setting up backing services (e.g. database containers), automated SSL certificate provisioning and renewal, zero-downtime deploys, etc.

The setup involves specifying a bunch of configuration in a YAML file – things like your server's IP address, where to fetch your application's Docker image from, environment variable definitions, etc. Once the setup is done, you can run kamal deploy and have an updated version of your application running on your server within just a few minutes. And if your application requires a backing service (such as a PostgreSQL database), Kamal lets you define "accessories" in the configuration file that are ultimately containers that run alongside your main web application, providing a specific service. So, for instance, if you're running a Django application that requires PostgreSQL, you could define a postgres accessory that your Django application can connect to.

Once you have the workflow set up, it's really quite painless.

Defining a PostgreSQL accessory

The previous section handwaved accessories, but here's an example YAML snippet that hopefully makes the PostgreSQL setup in a Kamal context a bit more tangible.

accessories:
  postgres:
    image: postgres:18
    host: 1.2.3.4
    port: "127.0.0.1:5432:5432"
    env:
      clear:
        POSTGRES_DB: example
        POSTGRES_USER: example
      secret:
        - POSTGRES_PASSWORD
    directories:
      - data:/var/lib/postgresql

This snippet instructs Kamal to fetch the postgres:18 Docker image from the Docker hub and run a PostgreSQL container alongside your main web application. The environment variables defined under the env key will be passed on to the postgres container and the port/directory mapping will be done as defined in the configuration.

Backing up PostgreSQL to Ionos Storage

If you're running a database you should also consider backing it up somewhere. With PostgreSQL, one simple and easy way is to run pg_dump on your database and store the result on an S3 bucket.

In the Kamal world, we can use an accessory to do this. The siemens/postgres-backup-to-s3 project provides a Docker image to periodically back up a PostgreSQL database to Amazon S3. We can reference that image to define and boot an accessory, that, once running, will perform automated database backups for us. It's meant for S3 buckets, but if the object storage provider of your choice is S3 compatible, that should work just as well.

Here's an example YAML snippet that shows how this can work in practice:

accessories:
  postgres-backup:
    image: siemens/postgres-backup-s3:18
    host: 1.2.3.4
    env:
      clear:
        SCHEDULE: "@daily"
        BACKUP_KEEP_DAYS: 7
        S3_ENDPOINT: https://s3.eu-central-3.ionoscloud.com
        S3_REGION: eu-central-3
        S3_BUCKET: example
        S3_PREFIX: backups
        POSTGRES_HOST: example-postgres
        POSTGRES_DATABASE: example
        POSTGRES_USER: example
      secret:
        - POSTGRES_PASSWORD
        - S3_ACCESS_KEY_ID
        - S3_SECRET_ACCESS_KEY

We define a postgres-backup accessory that uses the siemens/postgres-backup-to-s3 Docker image. The environment variables are all meant for the running container, defining the backup schedule, the S3 credentials, and details of the database that should be backed up. This piece of configuration will make sure that our database is backed up every night and the resulting file is storegd on the configured Ionos storage bucket.

Note that the S3_ENDPOINT variable does not have anything to do with Amazon S3, instead referencing an ionoscloud domain name. Ionos storage buckets are S3 compatible, meaning that if you have some code meant to run with S3, you can basically swap out the bucket URL and the S3 credentials and it should all just continue to work.

Conclusion

After using Kamal over the past few months, I've come to really like it. It takes away a lot of the grunt work associated with deploying user-facing web applications. The setup I described above has been running reliably for my side project, handling both the main database and the nighly backups without intervention. In case you're considering Kamal for your next deployment, I'd definitely recommend!