A sample Micronaut application to set up fast local development with hot reload inside docker.
We will use Maven and Gradle as the build tools for our application.
Ensure the working of hot reload in the local machine. For a micronaut application, it's pretty straight forward i.e, we don't need to add any dependency or plugin, just need to run the maven/gradle command inside the working directory:
Maven:
./mvnw mn:run
Gradle:
./gradlew run -t
Here -t
enable continuous build which means Gradle should re-build the project if there are any changes
triggered in src directory.
Maven / Gradle:
While the application is running, change the code base by adding some System.out.println
statement/statements and
save the changes. Then the changes should be auto-compiled and updated without having to
restart the application(hot reload).
Create Dockerfile
, docker-compose.yml
, and .env
(used to pass values to variables used inside
docker-compose.yml) inside the working directory.
The project structure after this looks like:
<working-dir>
├── ...
├── src
| └── ...
├── .env
├── Dockerfile
├── docker-compose.yml
└── README.md
Maven:
Gradle:
Now, copy the content from Dockerfile
, docker-compose.yml
and .env
files (based on build tool) to newly
created Dockerfile and docker-compose.yml files in your project.
Make appropriate changes in docker-compose.yml
and .env
like:
docker-compose.yml:
version: '3.8'
services:
spring-boot-postgres-maven:
image: <your-image-name>
container_name: <your-container-name>
....
.env:
...
DB_NAME=<your-db-name>
...
Maven/Gradle:
Now, run the application inside docker:
Simply do docker-compose up
inside the working directory
$ docker-compose up
Then you may get an error like permission denied exception for ./mvnw file
or permission denied exception for ./gradlew file
To solve this issue, execute the command:
chmod u+x ./mvnw
Similarly, for Gradle do chmod u+x ./graldew
Again do a docker-compose up
, this time you don't get any errors but
for the first build, it may take up to 7-10 minutes to pull
images(openjdk:11 and postgres:14.1-alpine) and download dependencies. If everything runs
successfully, by doing docker ps
you
would see a similar outcome(image and container names may differ) for maven / gradle:
➜ micronaut-postgres ✗ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
c30ca301b5dd micronaut-postgres-image "./gradlew run -t" About an hour ago Up About an hour 0.0.0.0:8000->8000/tcp, 0.0.0.0:8080->8080/tcp micronaut-postgres-container
263e83e4296f postgres:14.1-alpine "docker-entrypoint.s…" About an hour ago Up About an hour 0.0.0.0:5432->5432/tcp postgres-container
Now, while the application is up and running inside docker, make the changes to the code base, then you would see that the application running inside docker should restart automatically.
Logs inside docker after doing docker-compose up
:
.......
micronaut-postgres-container | Listening for transport dt_socket at address: 8000
micronaut-postgres-container | __ __ _ _
micronaut-postgres-container | | \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
micronaut-postgres-container | | |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
micronaut-postgres-container | | | | | | (__| | | (_) | | | | (_| | |_| | |_
micronaut-postgres-container | |_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
micronaut-postgres-container | Micronaut (v3.7.2)
.......
If you observe the log Listening for transport dt_socket at address: 8000
, that means the application
is running in debug mode at port 8000 inside docker.
The application is running in debug mode because of the command inside docker-compose:
docker-compose.yml
...
command: ./mvnw spring-boot:run -Dspring-boot.run.jvmArguments="-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:${DEBUG_PORT_ON_CONTAINER}"
...
In case of Gradle to run the application in debug mode add run
task to build.gradle
:
run task:
run {
jvmArgs=["-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:8000"]
}
After that do docker-compose up
inside the working directory, then the logs inside docker are:
...
micronaut-postgres-container | Starting a Gradle Daemon, 1 incompatible and 1 stopped Daemons could not be reused, use --status for details
micronaut-postgres-container | > Task :compileJava UP-TO-DATE
micronaut-postgres-container | > Task :processResources UP-TO-DATE
micronaut-postgres-container | > Task :classes UP-TO-DATE
micronaut-postgres-container |
micronaut-postgres-container | > Task :run
micronaut-postgres-container | Listening for transport dt_socket at address: 8000
micronaut-postgres-container | __ __ _ _
micronaut-postgres-container | | \/ (_) ___ _ __ ___ _ __ __ _ _ _| |_
micronaut-postgres-container | | |\/| | |/ __| '__/ _ \| '_ \ / _` | | | | __|
micronaut-postgres-container | | | | | | (__| | | (_) | | | | (_| | |_| | |_
micronaut-postgres-container | |_| |_|_|\___|_| \___/|_| |_|\__,_|\__,_|\__|
micronaut-postgres-container | Micronaut (v3.7.2)
.....
If you observe the log Listening for transport dt_socket at the address: 8000
, that means the application
is running in debug mode at port 8000 inside docker.
Maven / Gradle:
Inside jvmArgs we have used the property address=*:8000
which tells from where we should
attach a debugger. Here *
is
placed in the place of host which means we can attach to the debugger from any host within the same
network.
Then follow the steps in Remote Debugging Using IntelliJIDEA to attach a debugger.
version: '3.8'
services:
micronaut-postgres:
image: micronaut-postgres-maven-image
container_name: micronaut-postgres-maven-container
networks:
- student-grading-network
build:
context: .
env_file: .env
depends_on:
- db
ports:
- ${APPLICATION_PORT_ON_DOCKER_HOST}:${APPLICATION_PORT_ON_CONTAINER}
- ${DEBUG_PORT_ON_DOCKER_HOST}:${DEBUG_PORT_ON_CONTAINER}
volumes:
- ./:/app
command: ./mvnw mn:run -Dmn.debug -Dmn.debug.host=* -Dmn.debug.port=${DEBUG_PORT_ON_CONTAINER}
db:
container_name: postgres-container
image: postgres:14.1-alpine
env_file: .env
ports:
- ${DB_PORT_ON_DOCKER_HOST}:${DB_PORT_ON_CONTAINER}
volumes:
- db:/var/lib/postgresql/data
networks:
- student-grading-network
volumes:
db:
networks:
student-grading-network:
Here each service acts as a new container. Since our application is dependent on db
service, we
need
to take care of a few things:
micronaut-postgres
service shouldn't start beforedb
service. And that is why we useddepends_on
property undermicronaut-postgres
.micronaut-postgres
anddb
services both have to be on the same network, so that they can communicate with each other. If we don't provide any network to services, they might run in isolated networks which leads to communication link failure between the application and the database.- Finally, for hot reloading of the app inside docker, our current directory(where the source code exists) should be mounted to the working directory inside the container.
volumes:
- ./:/app
To run End-To-End(E2E) tests, we need to mock the server and database. One way to do that is by using test containers.
Adding docker-compose-test.yml
file would help to run the test inside docker. Then the project structure is:
<working-dir>
├── ...
├── src
| └── ...
├── Dockerfile
├── docker-compose.yml
├── docker-compose-test.yml
└── README.md
version: '3.8'
services:
tests:
image: maven:3
stop_signal: SIGKILL
stdin_open: true
tty: true
working_dir: /app
environment:
- TESTCONTAINERS_HOST_OVERRIDE=host.docker.internal
volumes:
- ./:/app
- /var/run/docker.sock:/var/run/docker.sock
# Maven cache (optional), if .m2 repository isn't mounted, maven will download all
# the dependencies mentioned in 'pom.xml' from mvn central repository.
- ~/.m2:/root/.m2
command: mvn clean test
Here ~/.m2
is specific to mac, if you're using a different platform, replace ~/.m2
with C:\Users\{your-username}\.m2
for windows or /root/.m2
for Linux.
Follow the command to run tests inside docker.
- Change the directory in Terminal or CMD to
<working-dir>
$ cd
<PATH-TO-WORKING-DIR>
- Run the
docker-compose-test.yml
file.
$ docker-compose -f docker-compose-test.yml up
If you're not mounting the .m2
then it would time take to download all the dependencies mentioned
in pom.xml
.
Once the dependencies are mounted or downloaded, you would see the following logs as a good sign -