[Docker] 인텔리제이에서 도커로 배포하기

개요top

지난 글에서 도커에 대해서 알아보고, 기초적인 명령어에 대해서 살펴봤습니다.

이 지식을 바탕으로 조금 더 실무적인 내용을 다루겠습니다.

이 글에서는 도커 컨테이너 배포에 대한 기초적인 방법을 소개합니다. 구체적으로 설명하자면, 인텔리제이에서 제작한 스프링 애플리케이션을 빌드하고 도커 이미지를 만든 다음, 로컬환경(자신의 컴퓨터)의 도커와 원격환경(테스트 서버등) 도커에 이미지를 올려 컨테이너로 실행시키는 가장 기본적인 내용을 다룹니다. 실습해 보고 자신의 개발 환경에 맞게 응용할 수 있습니다.

1. 기본 환경top

이 실습을 위한 기본 환경을 소개합니다.

  1. 윈도우 OS에 도커가 설치되어 있어야 합니다.
    지난 글에서 윈도우 환경에서 도커 설치 방법을 참고하세요. 그리고 설치된 도커가 실행중이어야 합니다. 도커 툴박스(구버전)가 설치되어 있는 경우는 Docker Quickstart Terminal이 실행되어 있어야 합니다. Docker for windows(새로운 버전)의 경우는 그냥 실행만 되어 있으면 됩니다.

  2. OpenJDK 1.8이 설치되어 있어야 합니다.
    OpenJDK 11이 아닌 1.8로 하시길 바랍니다.

  3. 인텔리제이 Ultimate(유료) 버전인 것을 가정합니다.
    무료 버전인 커뮤니티를 써도 되지만 이번 글에서는 스프링 부트 프로젝트를 자동으로 만들어주는 도구를 인텔리제이의 기능으로 사용하는 예제를 쓰기 때문입니다.

  4. 원격서버에 우분투 18.04 이상, 도커가 설치되어 있어야 합니다.

2. 인텔리제이에서 스프링부트 프로젝트 만들기top

OpenJDK 1.8이 설치되어 있고 인텔리제이 Ultimate(유료) 버전을 사용한다는 기준으로 설명드립니다.

2.1 스프링부트 프로젝트 생성

먼저 인텔리제이를 실행하고 메뉴 File > New > Project…에서 프로젝트를 생성하겠습니다.

New Project 창이 뜨면 왼쪽에 Spring Initializr를 선택합니다. 그리고 Project SDK는 1.8을 선택하세요. Initializer Service URL로 Default인 https://start.spring.io 를 선택합니다.

참고로 https://start.spring.io는 다음과 같은 웹사이트 입니다.

이 사이트(Spring Initializr)를 통해서 스프링 프로젝트를 만들어낼 수 있습니다. 인텔리제이 Ultimate(유료) 버전에서는 이 사이트를 사용해서 쉽게 스프링 프로젝트를 생성하도록 도와주는 기능이 있습니다. 만약 인텔리제이 커뮤니티(무료)버전을 사용하신다면 이 사이트에서 생성하셔도 됩니다.

다음으로 프로젝트 메타데이타를 다음처럼 설정합니다.

언어는 Kotlin, 패키징은 War, 자바 버전은 8, Version은 0.0.1로 합니다. 참고로 Kotlin을 몰라도 이번 실습하는데 큰 지장이 없습니다. 그냥 따라하시면 됩니다.

타입은 Gradle을 선택하세요. 여기서는 Gradle 기준으로 설명합니다.

다음으로 의존 프레임웍 및 라이브러리를 선택합니다.

여기서는 DevTools, Web, Thymeleaf 정도만 선택하겠습니다. Finish 버튼을 누르셔서 프로젝트를 생성하세요.

생성 중에 위와 같은 창이 떳다면 Use Auto-import 를 선택하세요.

프로젝트가 생성되고 Gradle이 구동되면서 빌드를 실시합니다.

위 화면처럼 되었다면 프로젝트 생성이 완료된 것입니다.

2.2 Hello World 스프링 부트 컨트롤러 만들고 실행해보기

이제 Hello 메세지를 출력하는 간단한 컨트롤러를 만들어 보겠습니다.

아래 화면에서 보여주는 것처럼 프로젝트 탭에서 src/main/kotlin/com/example/demo에 코틀린 클래스인 HelloController.kt를 만들고 src/main/resources/templates에 hello_name.html을 만듭니다.

HelloController.kr 코틀린 코드는 다음과 같습니다.

package com.example.demo

import org.springframework.stereotype.Controller
import org.springframework.ui.Model
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.ResponseBody

@Controller
class HelloController {

    @GetMapping("/hello")
    @ResponseBody
    fun hello_world(): String {
        return "hello, world"
    }

    @GetMapping("/hello/{name}")
    fun hello_name(@PathVariable name: String, model: Model): String {
        model.addAttribute("name", name)
        return "hello_name" // Thymeleaf 템플릿 이름
    }
}

컨트롤러 내부에 2개의 메소드를 만들었습니다.

  • hello_world() 메소드는 http://localhost/hello 로 접근하면 @ResponseBody 애노테이션을 썼으므로 반환값인 hello, world 문자열을 그대로 출력하도록 정의했습니다.
  • hello_name() 메소드는 http://localhost/hello/{name}으로 접근하면 인자값에 @PathVariable 애노테이션으로 지정된 name변수로 값을 받도록 했습니다. 그리고 모델에 name키로 name 함수인자값을 할당하고요. 이 메서드는 @ResponseBody를 사용하지 않았으므로 반환값은 템플릿 이름입니다. 즉, 반환문자열인 hello_name의 이름을 가진 템플릿 뷰를 요청합니다.

템플릿을 만들겠습니다. 템플릿인 hello_name.html 코드는 다음과 같습니다.

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Hello, [[${name}]]</title>
</head>
<body>
<p>Hello, [[${name}]]</p>
</body>
</html>

이것은 Thymeleaf 템플릿 규칙을 따릅니다. 내부에 [[${name}]]은 컨트롤러에서 줄 변수라고 보시면 됩니다. 앞서 컨트롤러에서 모델에 name키로 url path에서 준 값을 받았다는 것을 확인하세요.

이제 실행해 보겠습니다.

다음처럼 Gradle 창을 열어 Tasks/application에 bootRun을 더블 클릭하셔서 애플리케이션을 실행할 수 있습니다(만약 이 창이 안보이신다면 인텔리제이를 재실행하세요).

bootRun 대신 아래처럼 실행 버튼을 누르셔도 됩니다.

그럼 Run 창이 뜨면서 로그가 찍힐 것입니다.

로그를 잘 보시면 톰켓이 실행되었고 포트번호 8080이 열려있음을 알 수 있습니다.

이제 웹브라우저에서 접근해 보겠습니다. 아래 화면처럼 각각 컨트롤러에 정의된 메서드가 실행되어서 브라우저에 관련 데이타가 출력되었음을 확인할 수 있습니다.

원하는 데로 실행이 잘 됩니다. 기본 프로젝트 구현은 이것으로 마무리 합니다. 이제 도커 빌드에 대해서 이야기 해봅니다.

3. 도커 빌드를 위한 jib 플러그인top

3.1 jib 소개

Jib은 구글에서 제공하는 Java 애플리케이션 용 도커 및 OCI 이미지를 작성하기 위한 Gradle 플러그인입니다. 도커 빌드 환경을 구축하는 일은 참 귀찮고 어렵습니다. Jib는 이 귀찮은 것을 말끔히 해소해 줍니다.

Jib – Containerize your Gradle Java project

참고로 Maven을 위한 Jib 라이브러리는 따로 있습니다.

3.2 jib으로 스프링 부트 프로젝트를 도커 이미지로 빌드하기

우리는 Gradle 기반으로 만들었으므로 Jib Gradle을 추가해 보겠습니다. 먼저 build.gradle 파일을 엽니다.

그리고 plugins에 id ‘com.google.cloud.tools.jib’ version ‘1.0.0’를 추가합니다.

plugins{ 
     .....
    id 'com.google.cloud.tools.jib' version '1.0.0'
}

그러면 인텔리제이의 Gradle 탭에서 Tasks/other 폴더에 다음과 같은 jib관련 태스크가 보이게 됩니다.

jib로 도커 이미지를 만들어 보겠습니다. 먼저 war 파일을 만들어야 합니다. /Tasks/build 폴더에 있는 bootWar 태스크를 더블클릭으로 실행하세요.

그러면 Run 콘솔로 빌드 과정을 보여주면서 빌드 합니다. 다음처럼 BUILD SUCCESSFUL 메시지를 보면 WAR 생성을 성공한 것입니다.

아래처럼 프로젝트에 build/libs 폴더를 보면 생성한 demo-0.0.1.war를 확인할 수 있습니다.

이제 jib 도커 이미지를 만들어 보겠습니다. Gradle 탭에서 Tasks/other에 있는 jibDockerBuild를 더블 클릭해서 실행하세요.

그럼 다음처럼 Run 콘솔에 빌드 과정을 보여주며 도커 이미지를 만들어 냅니다. 다음처럼 BUILD SUCCESSFUL 메세지가 뜨면 도커 이미지가 만들어 진 것입니다.

도커 이미지를 확인하겠습니다. 제 컴퓨터에는 도커 툴박스가 설치되어 있기 때문에 Docker QuickStart Terminal 에서 확인하겠습니다.

docker images

명령으로 등록된 이미지를 확인하세요.

리스트 중 마지막에 demo 도커 이미지가 잘 등록되어 있음을 확인할 수 있습니다. 이 도커 이미지를 컨테이너로 올려 실행해 보겠습니다.

이때 쓰는 도커 명령은 run 입니다.

docker run -d -p 80:8080 –name my-demo demo:0.0.1

도커 명령 docker ps로 확인해보니 컨테이너로 제대로 생성되어 실행되었습니다. 이제 웹브라우저에서 확인해 보겠습니다.

이렇게 jib를 이용하면 너무나 쉽게 도커 이미지를 만들 수 있습니다.

참고로, 도커 툴박스가 아닌 환경에서는 192.168.99.100 이 아니라 localhost를 쓰시면 됩니다.

방금 실행한 Gradle 태스크인 bootWar와 jibDockerBuild를 인텔리제이 UI에서 더블클릭으로 실행했지만 인텔리제이의 터미널(Terminal)에서 이 명령을 직접 타이핑해서 실행할 수 있습니다. 그리고 터미널에서는 docker 명령도 직접 쓸 수 있습니다.

docker 명령은 아래 화면처럼 터미널을 직접 열고 실행할 수 있습니다.

그리고 Gradle 태스크는 gradlew를 이용해 실행할 수 있습니다.

만약 터미널에서 gradlew jibDockerBuild 수행시 allowInsecureRegistries 관련 에러가 난다면 build.gradle 파일의 맨 아래에 다음 코드를 추가하세요.

jib{
    allowInsecureRegistries = true
}

jib를 사용한 스프링 프로젝트를 도커 이미지 만들어 컨테이너로 실행하는 기본적인 실습을 마치겠습니다.

3.3 jib 옵션 지정하여 빌드하기

jib를 빌드할 때 다음 코드처럼 여러가지 옵션을 지정할 수 있습니다. 방금 보여드린 allowInsecureRegistries 도 옵션의 한 예였습니다.

jib {
  from {
    image = 'openjdk:alpine'
  }
  to {
    image = 'localhost:5000/my-image/built-with-jib'
    credHelper = 'osxkeychain'
    tags = ['tag2', 'latest']
  }
  container {
    jvmFlags = ['-Xms512m', '-Xdebug', '-Xmy:flag=jib-rules']
    mainClass = 'mypackage.MyApp'
    args = ['some', 'args']
    ports = ['1000', '2000-2003/udp']
    labels = [key1:'value1', key2:'value2']
    format = 'OCI'
  }
}

여러분은 이 옵션을 jib 공식 문서에서 직접 학습할 수 있습니다.

만약 여러분이 옵션을 설정한다면 아래 코드 정도로 하면 될 것 같습니다.

jib {
    allowInsecureRegistries = true
    to {
        image = "${project.name}:${project.version}"
    }
    container {
        // Set JVM options.
        jvmFlags = ['-XX:+UnlockExperimentalVMOptions', '-XX:+UseCGroupMemoryLimitForHeap', '-Dserver.port=8080']
        // Expose different port.
        ports = ['8080']
        // Add labels.
        labels = [maintainer: 'jidolstar']
    }
}

옵션의 자세한 설명은 생략하겠습니다.

4. Gradle 태스크를 생성해 도커 이미지 배포하기top

이제 이 글의 하이라이트를 소개합니다.

지금까지 여러 과정을 거쳐서 빌드/실행을 했습니다. 이를 하나의 태스크로 묶어서 한방에 실행할 수 있다면 더욱 편하지 않을까요?

여기서는 먼저 로컬 도커 환경에서 빌드하고 배포하는 Gradle 태스크를 만들어 보겠습니다. 그런 다음 원격지에 있는 우분투 서버에 SSH로 접근해서 배포하는 샘플도 함께 만들어 보겠습니다.

한 번의 클릭으로 배포까지 이루어지는 마법 같은 일을 볼 수 있을 것입니다.

4.1 로컬 도커 환경에 배포하는 Gradle 태스크 만들기

로컬 도커 환경에 배포하는 Gradle 태스크를 만들어 보겠습니다.

앞서 Gradle명령으로 clean, bootWar, jibDockerBuild를 하면 자동으로 로컬 도커에 이미지까지 등록됨은 이미 앞서 확인했습니다. 그 이후 도커 컨테이너로 실행하는 것은 직접 타이핑으로 수행했습니다. 직접 타이핑 대신 Gradle 태스크를 만들어 도커 명령들을 실행하면 도커 컨테이너 등록이 훨씬 용이할 것입니다. 이를 달성하기 위해 build.gradle 을 열어 맨 아래에 다음 코드를 입력하세요.

// 실행방법
// gradlew clean bootWar jibDockerBuild dockerLocalRun --stacktrace
task dockerLocalRun(){
    doLast{
        def imageName = "${project.name}:${project.version}"
        def containerName = "my-demo"

        exec{ commandLine "docker stop ${containerName}".split(' ') }
        exec{ commandLine "docker rm ${containerName}".split(' ') }
        exec{ commandLine "docker run -d -p 80:8080 --name my-demo ${imageName}".split(' ') }

        println('\n도커 빌드 완료')
    }
}

dockerLocalRun이라는 Gradle 태스크를 만들었고 doLast 시점에 이미 실행중인 컨테이너를 중지(docker stop) 시키고 삭제(docker rm)한 다음 다시 실행(docker run)하고 있습니다.

인텔리제이 터미널에서 gradlew clean bootWar jibDockerBuild dockerLocalRun –stacktrace를 실행해 보겠습니다. 그럼 다음 화면처럼 순서대로 태스크들이 실행할 것입니다.

어떤가요? 수동으로 했던 3단계의 작업을 단 하나의 명령으로 수행할 수 있게 되었습니다.

터미널에서 입력하는 것도 귀찮다면 다음처럼 해볼 수 있습니다.

먼저, 아래처럼 인텔리제이에서 Edit Configuration을 선택합니다.

그리고 + 버튼을 누른 뒤, Gradle을 선택합니다.

아래 화면처럼 입력하거나 선택합니다.

  • Name : docker local deploy
  • Gradle project: demo
  • Tasks : clean bootWar jibDockerBuild dockerLocalRun
  • Argumants : –stacktrace

OK 버튼으로 저장 후, 실행해보겠습니다.

Run 창에 다음 처럼 실행됩니다.

정말 버튼 한 번 클릭만으로 로컬 환경에서 스프링 프로젝트를 빌드하고 War로 만든다음 도커 이미지를 만들어 컨테이너로 실행하는 전 과정을 수행했습니다.

참고로 진행상황을 출력하기 위해 println을 쓰면 Gradle 코드를 다음처럼 변경할 수 있겠습니다.

task dockerLocalRun(){
    doLast{
        def imageName = "${project.name}:${project.version}"
        def containerName = "my-demo"
        def cmd

        println("\n\$ ${cmd="docker ps"}")
        exec{ commandLine cmd.split(' ') }

        println("\n\$ ${cmd="docker stop ${containerName}"}")
        exec{ commandLine cmd.split(' ') }

        println("\n\$ ${cmd="docker ps"}")
        exec{ commandLine cmd.split(' ') }

        println("\n\$ ${cmd="docker rm ${containerName}"}")
        exec{ commandLine cmd.split(' ') }

        println("\n\$ ${cmd="docker run -d -p 80:8080 --name my-demo ${imageName}"}")
        exec{ commandLine cmd.split(' ') }

        println("\n\$ ${cmd="docker ps"}")
        exec{ commandLine cmd.split(' ') }

        println('\n도커 빌드 완료')
    }
}

docker ps 명령으로 실행중인 컨테이너도 확인할 수 있도록 해봤습니다. 여기에 단위 테스트나 인수 테스트를 더 포함하면 더욱 완벽한 빌드 환경이 될 것이라 생각이 듭니다.

물론 위 코드는 기존에 컨테이너가 이미 실행되고 있다는 가정이 있기 때문에 좀 더 보강해야 합니다. docker ps 명령을 통해 실행중인 컨테이너 이름을 문자열로 추출해서 종료시키는 전 과정을 담은 Gradle 코드를 직접 만들 수 있을 겁니다. Gradle은 기본적으로 Groovy라는 언어를 활용한 DSL을 스크립트를 사용합니다. 이 스크립트로 더욱 완성도를 높히시길 바랍니다.

Gradle을 잘 모르시겠다면 다음 사이트를 참고하시길 바랍니다.

4.2 원격 도커 환경에 배포하는 Gradle 태스크 만들기

원격 도커 서버에 도커 이미지를 배포하기 위한 준비과정으로 jibBuildTar 태스크를 사용합니다. 이 태스크를 실행하면 도커 이미지를 tar로 압축해서 줍니다. 이 태스크를 실행하기 전에 bootWar 태스크로 WAR를 먼저 생성해야 합니다.

bootWar, jibBuildTar를 차례대로 실행하면 프로젝트에서 build 폴더내에 jib-image.tar 압축 파일이 생성됩니다. 이 파일을 원격 서버에 FTP로 올리고 도커 명령어중 docker load를 이용하면 압축을 풀고 바로 도커 이미지로 등록해줍니다.

그럼 원격 서버에 이 tar 파일을 올리기 위해 어떻게 해야할까요? SSH 플러그인을 사용하면 됩니다.
자세한 내용은 아래 링크를 참고하세요.

Gradle SSH Plugin

이 SSH 플러그인을 사용하려면 build.gradle 파일에서 plugins에 다음을 추가해야 합니다.

plugins{
   ...(생략)
   id 'org.hidetake.ssh' version '2.9.0'
}

그리고 build.gradle과 같은 위치에 gradle.properties 파일을 만들고 다음 내용을 추가합니다.

//호스트 도메인 
dockerHost=my.domain.com

//포트번호 
dockerPort=22

//아이디 
dockerUser=myid

//비밀번호 
dockerPassword=mypasswd

//jib-image.tar를 업로드할 경로 
dockerDir=/mydir/docker

이 파일은 접속정보를 외부파일로 관리한다는 의미가 있습니다. 하지만 이처럼 비밀번호와 같은 민감한 정보를 노출하는 것은 그다지 좋은 방법은 아닙니다. 그렇기에 비밀번호 접속 방식보다는 RSA로 비대칭 암호화 방식을 쓰는 것을 추천드립니다. 보안은 주제에 벗어나므로 이 정도만 언급하겠습니다.

위처럼 만든 접속정보는 다음처럼 쓸 수 있습니다.
SSH 플러그인에서 사용할 수 있도록 build.gradle에 아래 코드를 입력해주세요.

remotes {
    test {
        role 'test server'
        host = project.properties['dockerHost']
        port = project.properties['dockerPort'].toInteger()
        user = project.properties['dockerUser']
        password = project.properties['dockerPassword']
        knownHosts = allowAnyHosts
    }
}

test 이름으로 SSH 접속 정보를 설정했습니다. 사용된 project.properties[‘…’]는 방금 만든 gradle.properties 파일에 정의된 값들입니다.

지금부터 원격 서버에 jibBuildTar 태스트로 만들어진 jib-image.tar을 SSH 플러그인을 통해 업로드하고 docker load 명령으로 이미지를 만든 다음, 컨테이너로 실행하는 전 과정을 수행하는 Gradle 태스크를 만들겠습니다. 태스크 코드는 다음과 같습니다.

//실행방법 
//gradlew clean bootWar jibBuildTar dockerTestRun --stacktrace
task dockerTestRun{
    doLast{
        ssh.run {
            session(remotes.test) {
                def command
                def imageName = "${project.name}:${project.version}"
                def containerName = 'my-demo'
                def host = project.properties['dockerHost']
                def buildPath = fileTree(dir: 'build').getDir()
                def tarName = "jib-image.tar"
                def inPort = "8080"
                def outPort = "8080"
                def routePort = "9999"
                def from = "${buildPath}\\${tarName}"
                def to = project.properties["dockerDir"]

                command = 'docker version'
                println "\n\$ ${command}"
                execute(command){r->println r}

                command = "ls ${to}"
                println "\n\$ ${command}"
                execute(command){r->println r}

                command = "rm ${to}${tarName}"
                println "\n\$ ${command}"
                execute(command, ignoreError:true){r->println r}

                println "\n\$ put from:${from}, into: ${to}"
                put from: from, into: to

                command = 'ls'
                println "\n\$ ${command}"
                execute(command){r->println r}

                command = "docker stop ${containerName}"
                println "\n\$ ${command}"
                execute(command, ignoreError:true){r->println r}

                command = "docker rm ${containerName}"
                println "\n\$ ${command}"
                execute command, ignoreError:true

                command = "docker rmi ${imageName}"
                println "\n\$ ${command}"
                execute command, ignoreError:true

                command = "docker load --input ${to}${tarName}"
                println "\n\$ ${command}"
                execute command, ignoreError:true

                command = "docker images"
                println "\n\$ ${command}"
                execute command, ignoreError:true

                command = "docker run -d -p ${inPort}:${outPort} --name=${containerName} ${imageName}"
                println "\n\$ ${command}"
                execute command, ignoreError:true

                command = "docker ps"
                println "\n\$ ${command}"
                execute command, ignoreError:true

                println('\n도커 업로드 완료')
                println("\nhttp://${host}:${routePort}/ 에 접속하세요.")
            }
        }
    }
}

좀 길지만 차근차근 따라오면 어렵지 않게 해석이 가능합니다. 중요한 부분만 설명드립니다.

위 코드에서 보면 아래와 같은 부분이 있습니다.

    ..(생략)
    ssh.run {
            session(remotes.test) {

    ..(생략)

이 부분은 SSH 플러그인을 실행하고 remotes.test에 설정된 정보로 접속하는 명령입니다.
session(){….} 블록 내부에서 실행되는 명령들은 생성된 세션을 통해 원격 서버에 수행되는 것들입니다.

jib-image.tar 파일을 올리는 부분은 다음과 같습니다.

    ..(생략)
   println "\n\$ put from:${from}, into: ${to}"
   put from: from, into: to
    ..(생략)

put 명령은 로컬에 있는 파일을 원격에 올릴 수 있게 해주는 SSH 플러그인의 명령입니다.

그래고 SSH 플러그인 명령 중 execute는 리눅스 명령을 수행합니다.

    ..(생략)
   execute command, ignoreError:true
    ..(생략)

이 코드에서 ignoreError:true는 에러가 나더라도 무시하겠다는 의미입니다.

이제 터미널에서 다음 명령을 수행하세요.

> gradlew clean bootWar jibBuildTar dockerTestRun --stacktrace

성공적으로 올렸다면 “도커 업로드 완료” 메세지와 함께 “http://도메인:포트/에 접속하세요” 메세지를 볼 수 있게 될 것입니다.

이제 브라우저에서 해당 서버에 접근해보세요. 여러분의 원격 접속 정보에 맞게 접근하면 되겠죠?

이 방법은 기존에 실행된 이미지와 컨테이너를 지워버리고 있습니다. 하지만 상황에 따라 남겨야 하는 경우도 반드시 있을 것입니다. 그러므로 그런 경우라면 위 코드를 상황에 맞게 수정해서 쓰시길 바랍니다.

그리고 인텔리제이에서 버튼 한번 클릭으로 실행 시키는 방법도 이미 소개했으니 꼭 해보시길 바랍니다.

5. 에러 로그 확인top

배포까지 다 되었는데 브라우저에서 실행해 보면 아래처럼 런타임 에러가 날 수 있습니다.

제 경우에는 503 Service Unavailable 에러네요. 이런 경우에는 서버에서 앱이 실행되다가 문제가 난 경우일 가능성이 큽니다. 그런 경우에는

docker logs {컨테이너명}

를 활용해 보세요.


# docker logs my-demo 
....(생략)
[2019-03-27 08:56:17][ERROR][o.s.boot.SpringApplication] - Application run failed
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'tomcatServletWebServerFactory' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/ServletWebServerFactoryConfiguration$EmbeddedTomcat.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory]: Factory method 'tomcatServletWebServerFactory' threw exception; nested exception is java.lang.NoClassDefFoundError: Could not initialize class org.apache.juli.logging.LogFactory
        at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:627)
        at org.springframework.beans.factory.support.ConstructorResolver.instantiateUsingFactoryMethod(ConstructorResolver.java:456)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.instantiateUsingFactoryMethod(AbstractAutowireCapableBeanFactory.java:1305)
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.c
....(생략)

결론top

이상으로 스프링부트 프로젝트를 인텔리제이에서 만들고 Gradle을 통해 로컬과 원격 환경에 도커 이미지를 올리고 실행하는 전 과정을 다뤄보았습다. 소개한 내용으로 실무에 쓰기에는 다소 부족한 부분이 있습니다만, 작은 조직에서 도커 기반으로 개발할 때 인프라를 구축하는데는 유용한 지식이라 생각합니다. 지속적 통합(CI)을 도입하기 전에 개발 수준에서 도커를 운영하고 싶은 때 좋은 선택이 될 것입니다.