Metabase is a great tool for easy dashboarding and analytics, it is open source meaning you can deploy it easily on your own infrastructure.
I currently host my metabase instance on scaleway serverless, but to save some money and for more flexibility, I’m going to move it to a 5$ hetzner server and deploy it with kamal, I’ll also set-up some basic backup of the metabase DB on an s3 bucket.
Kamal is great, it’s like the good old capistrano but for docker, and simpler. I like it because it’s only some commands over ssh on your server(s), to manage your images and their deployment. Besides some files on your server, it doesn’t even know it’s running kamal.
I’ll document the steps I used so you can follow along and perform a similar deployment, if you want to skip to the good stuff, here’s a gist with the whole config.
Let’s get started.
Set-up the server.
On your server, make sure you have docker and curl installed, that’s all kamal needs to manage your deployments.
Warning: the official metabase docker image does not work on arm64 architectures, we’ll assume your server has x86 arch.
Create a new repository
We’ll need some code to manage this deployment, so let’s create a repository to hold it.
A very short Dockerfile
Kamal can’t quite yet deploy public images on the fly. We still need to build and push an image of our own.
We’ll just piggy back on the official metabase image with the version we want.
A docker repository
Kamal works by building docker images, then pulling them on the server. In the meantime, they need to be stored in a docker repository.
You can create public repositories for free on hub.docker.com
A basic configuration file.
Let’s start with a very simple configuration file and we’ll iterate to achieve all the features we need.
For simplicity, we need this file to be in a new repository, and located at config/deploy.yml
Let’s break it down a bit.
Image and hosts
Nothing too fancy, here we are declaring our service name and the docker image we want to use for it (from our newly created repository). We are also declaring one web server, replace the IP with your own ;)
Registry options
We need to provide kamal with our docker credentials, so it can pull our image. Your docker password will be set in the environment as a secret variable.
Environment
Here we are setting some configuration variables needed by metabase. We want to use postgres for the metabase database, we also configure the web port to be 3000, which is expected by kamal to expose the web server.
I allocated 2g of ram for the JVM through the JAVA_OPTS
, YMMV.
We also expose the MB_DB_CONNECTION_URI
variable, this is the string that contains the connection URI to postgres, including the password, hence setting it as a secret variable. More on that later.
Health check
Kamal checks for the health of the app before deploying it. Metabase provides a /api/health
endpoint that will return 200 once it is fully booted, that’s exactly what we want !
we set max_attempts
to 20 because metabase can take quite a while to boot up, especially if it has to run data migrations.
Accessories
Kamal allows us to have “accessories” living alongside our application, that are not build nor restarted along the app when it is deployed. It is a perfect use case for a database, a monitoring service, etc.
Here we set-up a the postgres database that our metabase instance will use.
We set a “directory”, so the PG data is persisted on our server disk.
We set some env vars to define the default user and db name. its password will be stored in the POSTGRES_PASSWORD
secret env variable.
Fill your env
Speaking of, now is a great time to fill our secret env variables.
Kamal will read them in the .env
file :
POSTGRES_PASSWORD=hunter2
MB_DB_CONNECTION_URI=postgres://metabase:hunter2@1.2.3.4/metabase
KAMAL_REGISTRY_PASSWORD=your_docker_password_wow_scary
Make sure this file is not checked into your git repository !
Starting up our accessories and service
Kamal provides a nifty kamal setup
command to perform all the preliminary tasks : push the env, create accessories, build and deploy our service.
kamal setup
If everything worked as supposed, at the end of the setup process, you should be greeted by the usual metabase welcome screen.
Optional : restore an existing DB
Skip this step if you want a fresh metabase instance.
I had an existing metabase instance, and I wanted to keep my queries, setup, etc.
First, I stopped the app to be sure nothing would be reading/writing on the db :
kamal app stop
Then, I re-created the pg accessory from scratch, effectively dropping the DB and creating a fresh one :
kamal accessory reboot pg
I then used pg_restore
to restore a previously created dump, and redeployed the app with kamal deploy
Setting-up SSL
SSL config has always been daunting to me, and the kamal doc does not mention it at all yet. Thankfully, it is actually quite simple, thanks to this article written by Guillaume Briday.
Let’s run the following command to create the appropriate files on our server.
mkdir -p /letsencrypt &&
touch /letsencrypt/acme.json &&
chmod 600 /letsencrypt/acme.json
Then, we need to add some config in our servers
block :
Finally, we need to add some config for traefik, the web server that kamal uses to expose our application.
Let’s re-deploy our app and restart traefik to pick up the changes:
kamal deploy
kamal traefik reboot
Your metabase instance should now be served over https.
Backing up your database to an S3-compatible remote bucket
Running stuff on your own servers is great, but it has its downsides.
Managing DB backups is one of them.
But with some ingenuity we can set it up pretty easily.
We can use postgres-backup-s3
, a nifty docker container that will backup any PG db to any S3 bucket.
Sounds like a kamal accessory 🧐
Let’s add it :
Check out the README for more configuration options.
Don’t forget to add S3_ACCESS_KEY_ID
and S3_SECRET_ACCESS_KEY
to your .env
file and push it with kamal env push
Then you can boot the accessory with kamal accessory boot s3_backup
To make sure everything worked, I started by omitting the SCHEDULE
var, so that the backups perform instantly, then I re-set it and waited a day to make sure the cron worked as expected.
Troubleshooting
Kamal is essentially a wrapper around docker, so for troubleshooting, I used docker logs
extensively, it can help figure out why a container is not working as expected. Coupled with docker ps
to see what is going on, as well as the kamal log, you should fairly easily understand what is going on.
Conclusion
Kamal makes it easy to deploy anything anywhere ! I especially love the accessory concept which allow to deploy quite complex applications.
One downside I see for now, is that you are forced to build and push your own images, I initially tried to skip this part and deploy from the public image, but kamal really expects the image to have been built from itself (it looks up for specific container/image labels and tags).
Kamal is still quite new, I’m convinced these kinds of issues will be addressed fairly soon !
You can find the whole config on this gist