Привет, друзья. Я всегда, когда работаю с каким-то популярным, публичным решением, например, как с Docker Compose, думаю, что нет нерешаемых проблем. Сегодня я встретился с таковой.
Итак, пишу до боли простое Java приложение: Spring boot + jOOQ + Flyway. Оно запускается в Docker-контейнере, который управляется с помощью Docker Compose. На хосте (то есть, не в контейнере) есть поднятая PostgreSQL, которая любезно слушает 5432 порт на localhost.
Давайте посмотрим на мой build.gradle, тут это важно:
buildscript { ext { springBootVersion = '1.5.9.RELEASE' } repositories { mavenCentral() jcenter() maven { url "https://plugins.gradle.org/m2/" } } dependencies { classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") classpath 'org.postgresql:postgresql:9.4.1212' classpath "nu.studer:gradle-jooq-plugin:2.0.7" classpath "gradle.plugin.com.boxfuse.client:flyway-release:4.2.0" } } apply plugin: 'java' apply plugin: 'idea' apply plugin: 'application' apply plugin: "org.flywaydb.flyway" apply plugin: 'org.springframework.boot' apply plugin: "nu.studer.jooq" group 'jdbc-test' version '1.0-SNAPSHOT' mainClassName = "ru.hixon.Main" sourceCompatibility = 1.8 repositories { mavenCentral() } dependencies { compile('org.postgresql:postgresql:9.4.1212') runtime('org.postgresql:postgresql') compile group: 'org.flywaydb', name: 'flyway-core', version: '4.2.0' compile('org.postgresql:postgresql:9.4.1212') compile('org.springframework.boot:spring-boot-starter-jooq') compile('org.springframework.boot:spring-boot-starter-web') runtime('org.springframework.boot:spring-boot-devtools') runtime('org.postgresql:postgresql') testCompile('org.springframework.boot:spring-boot-starter-test') jooqRuntime 'org.postgresql:postgresql:9.4.1212' compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.4' compile group: 'commons-io', name: 'commons-io', version: '2.6' compile "io.github.openfeign:feign-core:9.5.0" compile "io.github.openfeign:feign-jackson:9.5.0" compile "com.fasterxml.jackson.core:jackson-databind:2.6.4" compile "com.fasterxml.jackson.module:jackson-module-parameter-names:2.6.4" compile "com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.6.4" compile "com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.6.4" compile "io.github.openfeign:feign-slf4j:9.5.0" compile "com.google.code.findbugs:jsr305:3.0.2" } ext { databaseUrl = 'jdbc:postgresql://localhost:5432/jdbc-test' databaseLogin = 'jdbc-test' databasePassword = 'strongPassword' } processResources { expand(project.properties) } jooq { csmart(sourceSets.main) { jdbc { driver = 'org.postgresql.Driver' url = databaseUrl user = databaseLogin password = databasePassword schema = 'public' } generator { name = 'org.jooq.util.DefaultGenerator' strategy { name = 'org.jooq.util.DefaultGeneratorStrategy' } database { name = 'org.jooq.util.postgres.PostgresDatabase' inputSchema = 'public' } generate { relations = true deprecated = false records = true immutablePojos = false fluentSetters = true } target { packageName = 'ru.hixon.gen' directory = 'src/main/generated/java' } } } } flyway { url = databaseUrl user = databaseLogin password = databasePassword schemas = ['public'] locations = ["filesystem:$project.projectDir/src/main/resources/db/migration"] }
Какие можно сделать выводы из этого build.gradle? Наверное, если вы не робот, или компилятор — то никаких. А давайте с помощью прекрасного плагина для gradle — com.dorongold.task-tree — построим Граф Задач для двух нужных нам Тасок — build (сборка проекта) и bootRun (запуск проекта).
Вот Дерево Задач для Сборки проекта:
------------------------------------------------------------ Root project ------------------------------------------------------------ :build +--- :assemble | +--- :bootRepackage | | +--- :findMainClass | | | \--- :classes | | | +--- :compileJava | | | | \--- :generateCsmartJooqSchemaSource | | | | \--- :flywayMigrate | | | \--- :processResources | | \--- :jar | | \--- :classes | | +--- :compileJava | | | \--- :generateCsmartJooqSchemaSource | | | \--- :flywayMigrate | | \--- :processResources | \--- :jar | \--- :classes | +--- :compileJava | | \--- :generateCsmartJooqSchemaSource | | \--- :flywayMigrate | \--- :processResources \--- :check \--- :test +--- :classes | +--- :compileJava | | \--- :generateCsmartJooqSchemaSource | | \--- :flywayMigrate | \--- :processResources \--- :testClasses +--- :compileTestJava | \--- :classes | +--- :compileJava | | \--- :generateCsmartJooqSchemaSource | | \--- :flywayMigrate | \--- :processResources \--- :processTestResources
А вот Дерево Тасок для Запуска проекта:
:bootRun +--- :classes | +--- :compileJava | | \--- :generateCsmartJooqSchemaSource | | \--- :flywayMigrate | \--- :processResources +--- :findMainClass | \--- :classes | +--- :compileJava | | \--- :generateCsmartJooqSchemaSource | | \--- :flywayMigrate | \--- :processResources \--- :flywayMigrate
Какие выводы можно сделать из этих двух деревьев? Самые неутешительные! В обоих случаях нам нужно успешное соединенеие до базы данных — localhost:5432/jdbc-test. Это связано с тем, что в обоих Деревьях есть таска — compileJava, которая зависит от generateCsmartJooqSchemaSource и flywayMigrate.
Отвлечёмся немного от Gradle, и вспомним, что этот пост — про Docker и Docker-compose. Давайте посмотрим на структуру моего проекта:
. ├── docker-compose.yml ├── jdbc-test │ ├── build │ ├── build.gradle │ ├── docker-compose.yml │ ├── Dockerfile │ ├── gradle │ ├── gradlew │ ├── gradlew.bat │ ├── settings.gradle │ └── src
То есть, всё супер стандартно. Есть корневая папка. В ней файлик docker-compose.yml, который описывает все сервисы проекта. Каждый сервис проекта содержится в отдельной папке (тут для примера показана папка jdbc-test). D этой папке находится Dockerfile — описание текущего image, а также дочерний docker-compose.yml — описание конкретного сервиса.
Например, так может выглядеть корневой docker-compose.yml:
version: '3' services: jdbc-test: container_name: jdbc-test build: ./jdbc-test hostname: jdbc-test restart: always ports: - 8084:8084 network_mode: "host"
Так может выглядить Dockerfile конкретного сервиса:
FROM openjdk:8 RUN mkdir -p /app/ COPY . /app WORKDIR /app RUN chmod 777 /app/gradlew RUN ./gradlew build CMD ["./gradlew", "run"] EXPOSE 8084
А вот так может выглядить дочерний docker-compose.yml:
jdbc-test: build: context: . ports: - "8084:8084"
И вот на этом моменте я словил 2 Особенности Докера, с которыми протрахался все выходные.
Я разрабатываю на Windows 10. Как известно, в Windows докер запускается не нативно, а в виртуалке. Поэтому, когда я попытался запустить этот проект —
docker-compose up -d --build
То поймал неожиданную для меня ошибку:
Download https://repo1.maven.org/maven2/org/jooq/jooq-meta/3.9.6/jooq-meta-3.9.6.pom Jan 20, 2018 8:10:06 PM org.jooq.tools.JooqLogger info INFO: Initialising properties : /app/build/tmp/jooq/config.xml Jan 20, 2018 8:10:11 PM org.jooq.tools.JooqLogger error SEVERE: Cannot read /app/build/tmp/jooq/config.xml. Error : Connection to localhost:5432 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connect ions. 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. at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl. java:262) at org.postgresql.core.ConnectionFactory.openConnection(ConnectionFactory.java:52) at org.postgresql.jdbc.PgConnection.<init>(PgConnection.java:216) at org.postgresql.Driver.makeConnection(Driver.java:404) at org.postgresql.Driver.connect(Driver.java:272) at org.jooq.util.GenerationTool.run(GenerationTool.java:261) at org.jooq.util.GenerationTool.generate(GenerationTool.java:203) at org.jooq.util.GenerationTool.main(GenerationTool.java:175) Caused by: java.net.ConnectException: Connection refused (Connection refused) at java.net.PlainSocketImpl.socketConnect(Native Method) at java.net.AbstractPlainSocketImpl.doConnect(AbstractPlainSocketImpl.java:350) at java.net.AbstractPlainSocketImpl.connectToAddress(AbstractPlainSocketImpl.java:206) at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:188) at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392) at java.net.Socket.connect(Socket.java:589) at org.postgresql.core.PGStream.<init>(PGStream.java:61) at org.postgresql.core.v3.ConnectionFactoryImpl.openConnectionImpl(ConnectionFactoryImpl. java:144) ... 7 more :generateCsmartJooqSchemaSource FAILED FAILURE: Build failed with an exception. * What went wrong: Execution failed for task ':generateCsmartJooqSchemaSource'. > Process 'command '/usr/lib/jvm/java-8-openjdk-amd64/bin/java'' finished with non-zero exit valu e 255 * Try: Run with --stacktrace option to get the stack trace. Run with --info or --debug option to get mor e log output. BUILD FAILED in 1m 37s 1 actionable task: 1 executed ERROR: Service 'jdbc-test' failed to build: The command '/bin/sh -c ./gradlew build' returned a n on-zero code: 1
Вот Ту Фак? — Подумал я. Ведь все контейнеры запущены в Хостовой сети — network_mode: «host», и подключение к localhost должно прекрасно работать. Я упустил один момент. Докер запускается в в виртуалке, котораяз запускается в винде. Поэтому localhost докера — видит только localhost виртуалки, но не винды. Об этом даже в документации написано:
If you are using Docker for Mac (or running Linux containers on Docker for Windows), the docker network ls command will work as described above, but the ip addr show and ifconfig commands may be present, but will give you information about the IP addresses for your local host, not Docker container networks. This is because Docker uses network interfaces running inside a thin VM, instead of on the host machine itself.
К счастью, этот баг можно пофиксить, причём даже несколькими способами. Один из них — это использовать не localhost во время разработки на Windows, а docker.for.win.localhost. В этом случае у вас всё будет работать.
Итак, шло время, разработку на Windows нужно было заканчивать, и деплой на Ubuntu уже становился неизбежностью. Ну, я же знал, что Докер — это универсальная среда. Если на винде всё работает, то, без базара, это запустится и на Линуксе. Ага. 10 раз. Запустилось, проверяй.
Делаю я, значит, свой любимый — docker-compose up -d —build на Линуксике, и… Ловлю ошибку:
SEVERE: Cannot read /app/build/tmp/jooq/config.xml. Error : Connection to localhost:5432 refused. Check that the hostname and port are correct and that the postmaster is accepting TCP/IP connect ions.
Что за херня? Подумал я. Ведь это уже не Windows. Тут нет промежуточной виртуалки. А контейнеры запускаются в режиме network_mode: «host». Зуб даю — всё должно работать. Но нет.
Оказывается, что network_mode растространяется именно на запущенный контейнер. То есть, когда приложение работает, localhost будет прекрасно доступен. А вот во время сборки — build stage, это совсем не так.
Ну, я решил не отчаиваться, не я же первый, кто столкнулся с такой травиальной проблемой? Пошёл в доки, увидел кучу build options, среди которых тут же нашёл —network — Set the networking mode for the RUN instructions during build. Я уже подумал, что Задача Решена, Квест пройден, можно выйти в падик и попить пивасика.
Решил сначала собрать контейнер в одиночном режиме, без Docker Compose. Пишу, значит:
docker build --network=host -t jdbc-test.
И теперь всё прекрасно работает. Ещё бы, ведь я указал, что нужно использовать Хостовую Сетку во время сбора проекта.
Теперь пришло время подружить всё это дело с Docker-Compose… И, это то самое место, которое я не смог решить. По какому-то непонятному мне стечению обстоятельств, нельзя задать произвольные опции сборки. Всё, что вы можете — это задать пару опций непосредственно для build команды:
Usage: build [options] [--build-arg key=val...] [SERVICE...] Options: --force-rm Always remove intermediate containers. --no-cache Do not use cache when building the image. --pull Always attempt to pull a newer version of the image. -m, --memory MEM Sets memory limit for the bulid container. --build-arg key=val Set build-time variables for one service.
А также в определении сервиса в docker-compose.yml:
То есть, нет способа задать опцию Билда для Docker-compose. Я даже вопрос на stackoverflow задал — https://stackoverflow.com/q/48366594/1756750. Никто так и не ответил.
К слову, в Репозитории Docker-compose даже есть осбуждение на эту тему — https://github.com/docker/compose/issues/4114. Там один из участников предлагает реализовать такую форму задания опций билда:
build: context: . options: - no-cache
Из всей этой большой болтовни можно вынести один простой вывод. Даже в супер популярном программном обеспечении есть вещи, которые попросту нельзя реализовать, если не пойти самому в исходый код. Но это уже совсем другая история.
Категории: Программирование