When developing self-hosted open-source service, which meant to be used by as many users as possible, you would probably want to provide a simple way how run your service. It should preferably work without any configuration so users can get it working as fast as possible.🔥
The simplest way to achieve this with Docker is running a Docker image with docker run
command.
When your application needs database system to store its data, you have basically two options. Using multiple containers - one for the application and one for DB. Or you can go with embedding the DB service in the the same container with application. 📦
For deploying the application to the cloud, I recommend running it in multiple containers or use database managed by your cloud provider. In Tolgee we use Kubernetes for deploying our Application container and DigitalOcean's managed PostgreSQL DB, so we don't have to worry about the database cluster.
But for users who want to self-host it themselves, we also want to provide the simple docker run
way which is going to work right away without setting up a database system or any other services. It just has to run! Now! 🏃♂️ 🏃♀️
So if you would like to run Tolgee, you can do it simply by running this command:
docker run -v tolgee_data:/data/ -p 8085:8080 tolgee/tolgee
Before, we supported H2 and Postgres databases at the same time. We used H2 database as a default, since H2 doesn't need to be run as external service. It is just Java library which can store the data in memory or in filesystem. However, H2 db doesn't satisfy our needs anymore, so we have to drop its support.🤷♂️
OK, but we want to keep the ability to run it in single container, so how to do that?
Tolgee is implemented in Kotlin which is JVM language, so we need JDK to run it. So we need to somehow combine JDK and PostgreSQL images and then find a way how to run both of the services in the same container properly.
Getting the Postgres DB to our JDK container 🍱
To create our Docker image, we are using Dockerfile based on openjdk:14-jdk-alpine
image. So first thing we have to do is add a PostgreSQL DB to this image to have both JDK and Postgres prepared. To do so, I found official Postgres image sources and used their code to create my own image containing JDK and Postgres. Basically I just took their Dockerfile
and docker-entrypoint.sh
and replaced the FROM alpine:3.15
with FROM openjdk:14-jdk-alpine
. Thats it. Now we have Postgres and JDK in the same image.
HERE is the Docker image source
Running multiple services in a single container 🏃♂️ 📦
Second, the harder part is to actually run the services in single container.
There are multiple options according to Docker documentation how to achieve this.
1. Creating a script running the services 📜
#!/bin/bash
# Start the first process
./my_first_process &
# Start the second process
./my_second_process &
# Wait for any process to exit
wait -n
# Exit with status of process that exited first
exit $?
This basically runs all the services and if one exits, the whole container exits as well. This is cool, but this is not optimal since we want to:
- wait for Postgres to be started before running the app
- configure whether the database service is enabled or not, so user can use their own external Postgres
So we would have to modify this script a lot or find a better solution. Luckily, there are some.
2. Supervisor 🥸
Supervisor is a tool which can run and manage multiple services. It has a lot of features like redirecting stdout and stderr, managing how many times the services should try to start, or whether should service be restarted when exited. This is all cool, but I found few drawbacks trying to get it working with Supervisor.
- There is no option telling Supervisor to run service only if specific environment variable is set, so I would have to write a script for it
- There is no way how to tell Supervisor to exit when one of managed services exits. The supervisor is still running even after all services exited, which is in contrary with Docker philosophy. Containers should be managed by Docker and Docker should control whether services are restarted or not.
3. Letting the Spring Boot App to start the PostgreSQL server
Finally, I decided to do it a similar way as GitLab does it. I am letting the Spring Boot App to run the Postgres server while creating the DataSource bean. So the Spring Boot App is in charge of managing the DB.
*** HERE you can find the relevant code
Configuration (PostgresAutoStartConfiguration.kt)
First, I created a configuration, which is conditional on property tolgee.postgres-autostart.enabled
. This Configuration creates a DataSource
bean manually. And before this bean is returned, it uses my PostgresRunner
to run the Postgres DB.
The runner (PostgresRunner.kt)
This class handles the actual running of the Postgres executable. It does this:
- It runs the Postgres initialization script, which is taken from Postgres official Docker image code
- It starts a new thread which is redirecting Postgres output to Logger methods
- It waits until Postgres is ready by trying to open Socket on Postgres port
- It has preDestroy method annotated with PreDestroy annotation, which stops Postgres when Spring Application exits.
So that's it. This is how you can Embed Postgres into your application code! 🎉🎉🎉
TL;DR
- When you develop self-hosted service you would like to provide a simple way how to run it without complicated configuration steps
- So you want like to embed DB and Application into same container to enable users to run in with
docker run
command - First you have to create Docker image which is able to run your Application (JDK in our case) and contains also DB to store your data (PostgreSQL in our case)
- Then you have to find a way to start both of the services in the single container
- The best way I came up with is to run manage the PostgreSQL with the Java Application, so I can turn it on or of using externalized configuration
Tolgee is an open-source solution for software localization. It saves developer's time. Go to Tolgee.io and have fun!