Docker, Why?
In our previous chapter, our sample project used docker to provision a database.
This is no surprise that spin up a container rather a bare metal database is a simpler procedure, given the current state of modern container technology.
Containers became the de-facto standard way to package and deliver enterprise software. Public and private registries can support the software distribution and attend different hardware architectures as well.
They also hold a great advantage over full virtual machines: containers consumes less resources, since they still share most of the underlying operating system management.
Running containers
With docker properly installed, a simple command line is enough to run a container:
docker run hello-worldThis command runs the hello-world container image pulling it from the docker hub registry if it's not present on your system already.
With the command docker ps -a is possible to list all containers, running or not.
With docker image list is possible to see all images currently present.
Finally, docker image prune removes all unused docker images.
Proper use of a container image
It is paramount to read the documentation of an image to understand how to use it correctly.
For example, for the official postgres image:
docker run --rm -d \
--name pg-database -p 5432:5432 \
-e POSTGRES_PASSWORD=enterprise \
-e POSTGRES_USER=enterprise \
-e POSTGRES_DB=products \
postgres:16-alpineSome extra parameters are used:
--rmis used to create an ephemeral container.-dspawns the container as a daemon.--namedefines a name for the image instead of a random name.-pbinds container to host ports, the specified port in this case is the port used by postgres.-earguments creates environment variables used by this specific docker image, as described in the docs.
Creating a container image
It is important to know how to create a docker image to package enterprise software. As mentioned before, container images are the current standard for software distribution.
To create images using docker, it is important to understand how to write Dockerfiles:
FROM gradle:jdk21
ADD src /app/src/
ADD build.gradle.kts settings.gradle.kts /app/
WORKDIR /app
RUN gradle build
ENTRYPOINT gradle bootRunThe dockerfile above is intended to dwell at the project root folder. It expects a spring/kotlin gradle project, builds it and runs it. It's by no means properly optimized.
The command to build this image follows:
docker build -t my-enterprise-app .The -t parameter tags the image and the . is the build context (i.e. rom where files needed to the image will be copied).
Then you can run the created image:
docker run --rm my-enterprise-appTry it in this sample project to see it in action.
Use multi-stage builds
Container images doesn't need to provide a complete development environment. Instead, a minimal runtime is recommended to optimize resources consumption and provide better overall performance.
One way to achieve it is using multi-stage builds:
FROM gradle:jdk21 AS builder
ADD src /app/src/
ADD build.gradle.kts settings.gradle.kts /app/
WORKDIR /app
RUN gradle build
FROM eclipse-temurin:21-jre-alpine
COPY --from=builder /app/build/libs/project-011-spring-boot-example-0.0.1-SNAPSHOT.jar /app/boot.jar
WORKDIR /app
ENTRYPOINT java -jar boot.jarThat way the resulting container image will be much smaller.
Make it configurable with environment variables
Another key aspect to take in consideration is how to configure the application at runtime. Makes little sense, for example, build a new image whenever database credentials change.
A dockerfile similar to the previous one in this sample project will produce a valid image, but it fails to run due to the connection string:
FROM eclipse-temurin:21-jdk-alpine AS builder
ADD src /app/src/
ADD .mvn /app/.mvn/
ADD pom.xml mvnw /app/
WORKDIR /app
RUN ./mvnw package -Dmaven.test.skip=true
FROM eclipse-temurin:21-jre-alpine
COPY --from=builder /app/target/project015-0.0.1-SNAPSHOT.jar /app/boot.jar
WORKDIR /app
ENTRYPOINT java -jar boot.jarBuilding with docker build -t my-app-2 . and running with docker run --rm my-app-2 should produce the following error:
# ...
... 15 common frames omitted
Caused by: liquibase.exception.UnexpectedLiquibaseException: liquibase.exception.DatabaseException: org.postgresql.util.PSQLException: Connection to localhost:5432 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connections.
# ...In order to make it work, you must override the database url and expose the 8080 port:
docker run --rm \
-e SPRING_DATASOURCE_URL=jdbc:postgresql://192.168.1.182/products \
-p 8080:8080 \
my-app-2Every single spring-boot property can be overridden by an equivalent environment variable. So, the env var SPRING_DATASOURCE_URL overrides the spring.datasource.url property inside the application.properties file.
Finally, the 192.168.1.182 ip address is the current ip associated with the host machine. The localhost name resolves differently inside the container.
Publish image on docker hub or github registry or something else (ECR)
If reuse locally built images on other places is desired, then those images must be published into a docker registry.
The first step is to login at the registry. Then tag the image to publish accordingly. Finally, docker push the image:
docker login
# ...
docker tag my-enterprise-app:latest sombriks/my-enterprise-app
docker push sombriks/my-enterprise-appThe docker login without arguments logs into docker hub. To log into other registries simply pass the registry address in the command line:
# https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#pushing-container-images
docker login ghcr.ioEach registry expects a tag strategy. Check the desired registry documentation for details.
Further steps
For the next chapter will discuss infrastructure built over containers.