🐼

Docker環境で構築したフロントエンドとバックエンドの通信方法

2023/12/16に公開

はじめに

Docker環境でフロントエンドにNext.js、バックエンドにSpringBoot3(REST API)で開発していた際に、フロントエンドとバックエンドの通信でかなり躓いたので書き残します😇
フロントエンドとバックエンドの通信で苦労するところで一番最初に思い浮かぶのはオリジン間リソース共有(CORS)あたりではないでしょうか。
そこは私も類をもれずしっかりと苦労した点ではあるのですが、今回一番苦労したのはネットーワーク共有でした。。。
SpringBootのCORSについては以下記事が非常に分かりやすいです。
https://b1san-blog.com/post/spring/spring-cors/

環境について

  • SpringBoot 3.1.5
  • Next.js 14.0.1
  • Docker
  • dokcer-compose
  • macOS Sonoma 14.1.1

原因

結論からすると、ネットーワークの共有ができていなかったのが原因でした。
docker-composeファイルでnetworkを定義し、フロントエンドとバックエンドのネットワークを同じネットワークにすることで解決しました。

経緯

SpringBootでCORSの設定をし、いざDocker環境間での通信を試そうとした時、以下エラーが出ました。
▼ブラウザ
Warning: Prop className did not match. Server: "__className_e66fe9 vsc-initialized" Client: "__className_e66fe9"

▼サーバー
AxiosError: getaddrinfo ENOTFOUND {ホスト名}

エラーログ詳細
AxiosError: getaddrinfo ENOTFOUND share-favplace-api
    at AxiosError.from (webpack-internal:///(rsc)/./node_modules/axios/lib/core/AxiosError.js:85:16)
    at RedirectableRequest.handleRequestError (webpack-internal:///(rsc)/./node_modules/axios/lib/adapters/http.js:528:81)
    at RedirectableRequest.emit (node:events:515:28)
    at eventHandlers.<computed> (webpack-internal:///(rsc)/./node_modules/follow-redirects/index.js:21:28)
    at ClientRequest.emit (node:events:515:28)
    at Socket.socketErrorListener (node:_http_client:495:9)
    at Socket.emit (node:events:515:28)
    at emitErrorNT (node:internal/streams/destroy:151:8)
    at emitErrorCloseNT (node:internal/streams/destroy:116:3)
    at process.processTicksAndRejections (node:internal/process/task_queues:82:21) {
  hostname: 'share-favplace-api',
  syscall: 'getaddrinfo',
  code: 'ENOTFOUND',
  errno: -3008,
  config: {
    transitional: {
      silentJSONParsing: true,
      forcedJSONParsing: true,
      clarifyTimeoutError: false
    },
    adapter: [ 'xhr', 'http' ],
    transformRequest: [ [Function: transformRequest] ],
    transformResponse: [ [Function: transformResponse] ],
    timeout: 0,
    xsrfCookieName: 'XSRF-TOKEN',
    xsrfHeaderName: 'X-XSRF-TOKEN',
    maxContentLength: -1,
    maxBodyLength: -1,
    env: { FormData: [Function], Blob: [class Blob] },
    validateStatus: [Function: validateStatus],
    headers: Object [AxiosHeaders] {
      Accept: 'application/json, text/plain, */*',
      'Content-Type': undefined,
      'User-Agent': 'axios/1.6.1',
      'Accept-Encoding': 'gzip, compress, deflate, br'
    },
    method: 'get',
    url: 'http://share-favplace-api:8080',
    data: undefined
  },
  request: <ref *1> Writable {
    _writableState: WritableState {
      highWaterMark: 16384,
      length: 0,
      corked: 0,
      onwrite: [Function: bound onwrite],
      writelen: 0,
      bufferedIndex: 0,
      pendingcb: 0,
      [Symbol(kState)]: 34357356,
      [Symbol(kBufferedValue)]: null
    },
    _events: [Object: null prototype] {
      response: [Function: handleResponse],
      error: [Function: handleRequestError],
      socket: [Function: handleRequestSocket]
    },
    _eventsCount: 3,
    _maxListeners: undefined,
    _options: {
      maxRedirects: 21,
      maxBodyLength: Infinity,
      protocol: 'http:',
      path: '/',
      method: 'GET',
      headers: [Object: null prototype],
      agents: [Object],
      auth: undefined,
      family: undefined,
      beforeRedirect: [Function: dispatchBeforeRedirect],
      beforeRedirects: [Object],
      hostname: 'share-favplace-api',
      port: '8080',
      agent: undefined,
      nativeProtocols: [Object],
      pathname: '/'
    },
    _ended: true,
    _ending: true,
    _redirectCount: 0,
    _redirects: [],
    _requestBodyLength: 0,
    _requestBodyBuffers: [],
    _onNativeResponse: [Function (anonymous)],
    _currentRequest: ClientRequest {
      _events: [Object: null prototype],
      _eventsCount: 7,
      _maxListeners: undefined,
      outputData: [],
      outputSize: 0,
      writable: true,
      destroyed: false,
      _last: true,
      chunkedEncoding: false,
      shouldKeepAlive: true,
      maxRequestsOnConnectionReached: false,
      _defaultKeepAlive: true,
      useChunkedEncodingByDefault: false,
      sendDate: false,
      _removedConnection: false,
      _removedContLen: false,
      _removedTE: false,
      strictContentLength: false,
      _contentLength: 0,
      _hasBody: true,
      _trailer: '',
      finished: true,
      _headerSent: true,
      _closed: false,
      _header: 'GET / HTTP/1.1\r\n' +
        'Accept: application/json, text/plain, */*\r\n' +
        'User-Agent: axios/1.6.1\r\n' +
        'Accept-Encoding: gzip, compress, deflate, br\r\n' +
        'Host: share-favplace-api:8080\r\n' +
        'Connection: keep-alive\r\n' +
        '\r\n',
      _keepAliveTimeout: 0,
      _onPendingData: [Function: nop],
      agent: [Agent],
      socketPath: undefined,
      method: 'GET',
      maxHeaderSize: undefined,
      insecureHTTPParser: undefined,
      joinDuplicateHeaders: undefined,
      path: '/',
      _ended: false,
      res: null,
      aborted: false,
      timeoutCb: [Function: emitRequestTimeout],
      upgradeOrConnect: false,
      parser: null,
      maxHeadersCount: null,
      reusedSocket: false,
      host: 'share-favplace-api',
      protocol: 'http:',
      _redirectable: [Circular *1],
      [Symbol(kCapture)]: false,
      [Symbol(kBytesWritten)]: 0,
      [Symbol(kNeedDrain)]: false,
      [Symbol(corked)]: 0,
      [Symbol(kChunkedBuffer)]: [],
      [Symbol(kChunkedLength)]: 0,
      [Symbol(kSocket)]: [Socket],
      [Symbol(kOutHeaders)]: [Object: null prototype],
      [Symbol(errored)]: null,
      [Symbol(kHighWaterMark)]: 16384,
      [Symbol(kRejectNonStandardBodyWrites)]: false,
      [Symbol(kUniqueHeaders)]: null
    },
    _currentUrl: 'http://share-favplace-api:8080/',
    [Symbol(kCapture)]: false
  },
  cause: Error: getaddrinfo ENOTFOUND share-favplace-api
      at GetAddrInfoReqWrap.onlookupall [as oncomplete] (node:dns:118:26)
      at GetAddrInfoReqWrap.callbackTrampoline (node:internal/async_hooks:130:17) {
    errno: -3008,
    code: 'ENOTFOUND',
    syscall: 'getaddrinfo',
    hostname: 'share-favplace-api'
  }
}

解決策

  1. まずフロントエンドのdocker-compsoe.ymlでnetworksを定義します。
docker-compsoe.yml
services:
  app:
    build:
      context: .
      args:
        WORKDIR: ${WORKDIR}
        CONTAINER_PORT: ${CONTAINER_PORT}
        API_URL: "http://localhost:${API_PORT}/"
    container_name: share-favplace-app
    command: sh -c "npm run dev"
    volumes:
      - .:/${WORKDIR}
      - node_modules:/${WORKDIR}/node_modules
    ports:
      - "${FRONT_PORT}:${CONTAINER_PORT}"
    networks:
      - front

volumes:
  node_modules:

networks:
  front:
    external: false
  1. バックエンドのdocker-compose.ymlで先ほど作成したフロントエンドのネットワークを使用するように定義します。
docker-compsoe.yml
services:
  api:
    build:
      context: .
      dockerfile: Dockerfile
      args:
        WORKDIR: ${WORKDIR}
    container_name: share-favplace-api
    env_file:
      - ./environment/api-variables.env
    environment:
      ORG_GRADLE_PROJECT_debug: ${ORG_GRADLE_PROJECT_debug}
    tty: true
    command: ./gradlew clean bootRun
    volumes:
      - .:/${WORKDIR}
    ports:
      - "${API_PORT}:${API_PORT}"
      - "${DEBUG_PORT}:${DEBUG_PORT}"
    networks:
      - share-favplace_front
      - back
    depends_on:
      - db

networks:
  share-favplace_front:
    external: true
  back:
    external: false

フロントエンドで定義したネットワーク名は{アプリ名}_{docker-composeで定義したネットワーク名}となるため、share-favplace_frontを指定しています。(share-favplaceという名前でアプリを作成しています。)
external: trueとすることですでにあるネットワークを探してくれます。
external: falseとすると新しくネットワークを作成します。

以上でDocker環境間のネットワークの共有ができるようになります。

Discussion