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!