So I decided to containerize our application stack... Pt 2
Here's the story so far:
Beginning to work through my list
In the last post I came up with a list of tasks that I need to work through so that we can do things in Docker "correctly".
("Correctly" meaning, as close to "dev clones the repo, fills in their .env, then runs docker-compose")
Getting SQL Server to work in Docker on Linux
It's funny, this was actually pretty straight-forward. Faults aside over the years, Microsoft has been putting a lot of effort into building compatibility from their flagship products and also transitioning from a traditional software company to a platform company, and transitioning a lot of their major software to either being a SaaS offering (Outlook/O365) or making it run on Linux (SQL Server, Powershell) as well.
Running SQL Server is pretty easy, MS has good guides on SQL Server running on Linux on technet: https://docs.microsoft.com/en-us/sql/linux/quickstart-install-connect-docker?view=sql-server-2017&pivots=cs1-bash
It's super easy and all you really need to do is provide the password for the SA account as an environment variable.
Running Flyway migrations
Everything's easier when the vendor makes docker images easily available and usable.
Now, this is still a little tricky, because while the repo that has all our application code also has all our migrations, it's more than one migration and the flyway container comes expecting to be run via
docker run and just providing arguments to the migrate command.
That doesn't really work for me.
There's a couple of issues that I needed to overcome to get this service in my compose file working:
- I need the two databases the application expects
- I need to run the two migrations for our databases
By the time this service starts/runs, all I have is a bare SQL Server instance with no databases or accounts other than the default ones.
Normally, to create the two databases that our application needs, I just use sqlcmd or Invoke-SQLCmd, but this container doesn't have the first command and these are Linux containers and not Windows - so no Invoke-SQLCmd.
I started thinking about how (since I'm already using it in this container) I could abuse Flyway - since all it does is run specially named .sql files.
So, the first thing I did was create a new migration to do the initial Database creation. It was pretty straightforward since in the repo, there was already a script to create the base databases, I just needed to name it correctly (eg, VO__createDB.sql, and write the config file for this migration)
However, since this adds to the complexity of the problem, I need to update the above list:
- I need the two databases the application expects
- I need to run the
twothree migrations for our databases
Luckily, point two are why shell scripts exist.
I threw together a really basic shell script to run three flyway migrations. It's nothing fancy, but at the end of the day, it didn't need to be.
Now, the fun thing was trying to figure out how to overwrite the container's command and inject my shell script.
The nice thing is that Compose gives you exactly that.
The 'entrypoint' option just overrides the CMD of the container. In my case, giving me a one off container that runs the migrations.
Building and running the Tomcat Application
First and foremost, shoutout to Sammy for telling me about this.
My goal was to be able to have one Dockerfile to handle doing both the build, and also running the application.
Luckily, these days Dockerfiles have multi-stage builds (thanks for pointing that out, Sammy).
The important parts of this are the following:
FROM gradle:5.4-jdk8 as build
The first FROM line sets up the stage to copy artifacts from. In my instance, I can refer to it as 'build'.
The next important thing is the change to the COPY command.
COPY --from=build /home/gradle/project/heartbeatWeb/build/libs/heartbeat.war /usr/local/tomcat/webapps
The important part of this is the --from= argument. Normally, you use COPY to copy from the host to the container. When using the from argument, you can specify the first step and copy artifacts from that container instead.
Wiring everything up in Docker
I'm not up to doing anything particularly fancy yet (separate netoworks for separation of concerns or anything, so a basic network configuration in compose is more than enough.
That's more than enough to get all the containers into a state where they can talk.
web: build: . links: - db ports: - "80:8080" environment: - JAVA_OPTS=$JAVA_OPTS -Ddbserver=db depends_on: - flyway_migrate
This is the last container. From here, we see the links to the DB service, the the dependency on running the database migrations.
The default network is enough that you can address containers by their service name (see the -Ddbserver arg for the environment above).
Now, I can't stress this enough, this set up isn't for production. Everything is accessible. In the future, one of my items will be to separate internal services and external services, and to put things behind a proper proxy service (probably something like HAProxy, maybe Traefik if that can handle non-http traffic).
Task List Update
I did everything in my old task list, now I need a new one!
- Our Event Management Service
- Maybe clustered Ejabberd