Любую проблему можно решить, даже в Docker Compose

Привет, друзья. Я всегда, когда работаю с каким-то популярным, публичным решением, например, как с 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:

  • context
  • dockerfile
  • args
  • cache_from
  • labels
  • shm_size

То есть, нет способа задать опцию Билда для Docker-compose. Я даже вопрос на stackoverflow задал — https://stackoverflow.com/q/48366594/1756750. Никто так и не ответил.

К слову, в Репозитории Docker-compose даже есть осбуждение на эту тему — https://github.com/docker/compose/issues/4114. Там один из участников предлагает реализовать такую форму задания опций билда:

build:
  context: .
  options:
    - no-cache

Из всей этой большой болтовни можно вынести один простой вывод. Даже в супер популярном программном обеспечении есть вещи, которые попросту нельзя реализовать, если не пойти самому в исходый код. Но это уже совсем другая история.

Категории: Программирование