프롤로그
반드시 아래 두 개를 수행 한 뒤에 이 글을 수행하기를 빈다.
2025.02.13 - [웹 개발] - [SpringBoot] docker-java 사용하여 스프링부트로 도커 조작하기 - 사전준비
[SpringBoot] docker-java 사용하여 스프링부트로 도커 조작하기 - 사전준비
프롤로그스프링부트 공부한지 벌써 3개월째...회원가입 로그인 이런 것도 적을까말까 고민했는데 이미 노션에다가 정리하면서 적어두어서.... 졸업작품을 만들고 있는데, 내가 노션에다가 실패
taesan-smj.tistory.com
2025.02.13 - [웹 개발] - [SpringBoot] docker-java 사용하여 스프링부트로 도커 조작하기 - 컨테이너 생성(1/3)
[SpringBoot] docker-java 사용하여 스프링부트로 도커 조작하기 - 컨테이너 생성(1/3)
프롤로그여기서부터 공식 설명서 docs 들이 없다. 그래서https://github.com/docker-java/docker-java/tree/main/docker-java-api/src/main/java/com/github/dockerjava/api docker-java/docker-java-api/src/main/java/com/github/dockerjava/api at m
taesan-smj.tistory.com
저번 프롤로그 계획대로
총 3편으로 나눠서 컨테이너 생성 글을 작성할 계획인데
1편에서는 딱 컨테이너 생성하는 명령어만
2편에서는 컨테이너 실행과 포트 바인딩, 이미지 없을 때 불러오기
3편에서는 추가적인 생성시에 필요한 디테일
중에서 2편이다.
오늘 수행할 것은 이미지를 입력값에 맞춰서 불러오기, 컨테이너 생성 시에 진행하는 포트바인딩, 컨테이너 실행 및 프로세스 유지, 데이터베이스에 저장이 될 것 같다.
docker-java IMAGE API 알아보기
docker-java/docker-java-api/src/main/java/com/github/dockerjava/api/DockerClient.java at main · docker-java/docker-java
Java Docker API Client. Contribute to docker-java/docker-java development by creating an account on GitHub.
github.com
공식 문서 docker-java/api/DockerClient.java를 보면 111번째줄부터 IMAGE API 라고 적혀있는 부분에 가면 아래와 같은 명령어들이 보인다.
InspectImageCmd까지만 보자면
명령어 | 기능 | 터미널 명령어 |
pullImageCmd(String repository) | Docker Registry에서 이미지 가져오기 | docker pull <imageId> |
pushImageCmd(String name) | Docker Registry에 업로드 | docker push <이미지명> |
pushImageCmd(Identifier identifier) | Docker Registry에 Identifier를 이용한 업로드 | '' |
createImageCmd(String repository, InputStream imageStream) | 기존의 tar 파일을 기반으로 새로운 이미지 생성 | docker import backup-image.tar my-repo/my-new-image |
loadImageCmd(imageStream) | tar 파일에서 Docker 이미지 로드 | docker load -i backup-image.tar |
loadImageAsyncCmd(imageStream) | 비동기적으로 Docker 이미지 로드 | - |
searchImagesCmd(term) | Docker Hub에서 이미지 검색 | docker search <term> |
removeImageCmd(imageId) | 로컬 Docker 이미지 삭제 | docker rmi <imageId> |
listImagesCmd() | 로컬에 저장된 Docker 이미지 목록 가져오기 | docker images |
inspectImageCmd(imageId) | 특정 Docker 이미지의 상세 정보 확인 | docker inspect <imageId> |
이러한 이미지와 관련된 메서드들이 존재한다.
이미지를 입력값에 맞춰서 불러오기
저번 코드에서는 ubuntu:22.04만 지원을 하기 위해서 임의적으로
if(!("ubuntu").equals(createContainerRequestDto.getOs()) ||
!("22.04").equals(createContainerRequestDto.getVersion())){
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "현재는 Ubuntu 22.04만 지원합니다.");
}
이 코드를 넣어두었으나, 오늘 작업을 진행하는 것은 내 로컬에 해당 도커 이미지가 없더라도 실행시킬 수 있게 만들 것이기 때문에 지워버린다.
1. 이미지가 로컬에 존재하는 지 확인하기
로컬에 A라는 이미지가 존재하는지 확인하려면 어떻게 해야할까?
listImagesCmd() 를 통해서 목록을 가져오고 저 안에 A가 있는지 확인하면 되지않을까?
가장 확실한 방법은 inspactImageCmd()를 사용하는 것이다.
특정 Docker 이미지의 상세 정보 확인하는 명령어인데, 만약에 로컬에 해당 도커 이미지가 있다며 상세 정보를 가져오겠지만, 그렇지 않다면 찾을 수 없다는 오류를 출력할 것이다.
그를 토대로 아래와 같이 코드를 작성할 수 있다.
//이미지 존재 여부 확인하고, 없으면 다운로드
try {
dockerClient.inspectImageCmd(imageName).exec();
} catch (NotFoundException e) {
}
이미지가 존재하지 않다면 NotFound 에러가 발생할 테니 위와 같이 코드를 작성해줄 수 있다.
2. 이미지 불러오기
그 다음 이미지 불러오는 건 pullImageCmd(String repository) 이 명령어를 사용하면 된다.
docker-java/docker-java-api/src/main/java/com/github/dockerjava/api/command/PullImageCmd.java at main · docker-java/docker-java
Java Docker API Client. Contribute to docker-java/docker-java development by creating an account on GitHub.
github.com
해당 PullImageCmd의 공식문서를 보면, 괄호에다가 이미지를 넣고,
.withTag로 버전을 넣을 수 있고,
.exec(new PullImageResultCallback())을 넣을 수 있다.
PullImageResultCallback을 다시 보니
docker-java/docker-java-api/src/main/java/com/github/dockerjava/api/command/PullImageResultCallback.java at main · docker-java/
Java Docker API Client. Contribute to docker-java/docker-java development by creating an account on GitHub.
github.com
대충 이미지 다운로드 시작정도로 생각하면 된다. exec가 붙었다는 건 비동기로 처리하겠다는 의미이고, 그래서 해당 이미지가 다운 받는데 시간이 걸릴테니까 다운이 모두 끝날 때까지 기다리도록 해야하니까
ResultCallbackTemplate.java에 있는 86번째 줄의 저 awaitComplete를 써준다.
결과적으로 코드는 아래와 같이 수정된다.
//이미지 존재 여부 확인하고, 없으면 다운로드
try {
dockerClient.inspectImageCmd(imageName).exec();
} catch (NotFoundException e) {
System.out.println("이미지가 없어서 다운로드 중...");
try {
dockerClient.pullImageCmd(os)
.withTag(version)
.exec(new PullImageResultCallback())
.awaitCompletion();
} catch (Exception ex) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Docker 이미지 다운로드 실패", ex);
}
}
포트바인딩 명령어
다음은 포트 바인딩을 할 차례이다.
도커 컨테이너가 실행이 될 때, 내부적으로는 연결을 수행시키기 위해 컨테이너 내부는 22번 포트를 사용하고, 외부 포트는 우리쪽에서 자동 할당 되게 만들어주어야 한다.
docker-java/docker-java-api/src/main/java/com/github/dockerjava/api/model/PortBinding.java at main · docker-java/docker-java
Java Docker API Client. Contribute to docker-java/docker-java development by creating an account on GitHub.
github.com
이게 예전에 노션에 정리했었다가 다 날아가서 다시 정리하기 나도 진이 빠지기에 전지전능하신 지피티 형님의 말씀을 인용하겠다.
A. ExposedPort
- 컨테이너 내부에서 노출할 포트를 정의
ExposedPort containerPort = ExposedPort.tcp(22);
B. Binding (내부 클래스)
- Docker 호스트의 IP 주소와 포트 번호(또는 포트 범위)를 나타내기
- 주요 메소드 (정적 팩토리 메소드):
- bindPortSpec(String portSpec):포트 번호나 포트 범위를 문자열로 지정하여 Binding 객체 생성 (예: "80" 또는 "8000-9000").
- bindIp(String hostIp):호스트의 IP 주소만 지정하는 Binding 생성.
- bindIpAndPort(String hostIp, int port):IP 주소와 단일 포트 번호를 함께 지정하는 Binding 생성.
- bindIpAndPortRange(String hostIp, int lowPort, int highPort):IP 주소와 포트 범위를 지정하는 Binding 생성.
- bindPortRange(int lowPort, int highPort):IP 주소 없이 포트 범위만 지정하는 Binding 생성.
- bindPort(int port):IP 주소 없이 단일 포트 번호만 지정하는 Binding 생성.
- empty():빈 Binding을 생성합니다.
- parse(String serialized):Docker CLI에서 사용하는 문자열 형식(예: "127.0.0.1:80")을 파싱하여 Binding 객체를 생성
- 인스턴스 메소드:
- getHostIp():호스트 IP 주소 반환 (null이면 모든 인터페이스).
- getHostPortSpec():호스트 포트(또는 포트 범위)를 문자열로 반환.
- toString():[IP:]Port 형식으로 문자열 표현을 제공합니다.
C. PortBinding
- **역할:**하나의 컨테이너 내부 포트(ExposedPort)와, 해당 포트를 호스트에 바인딩하는 Binding을 연결하는 객체
- 주요 메소드:
- getBinding(): Binding 객체 반환.
- getExposedPort(): ExposedPort 객체 반환.
- parse(String serialized):문자열(예: "127.0.0.1:80:8080/tcp", "80:8080", "8080")을 파싱하여 PortBinding 객체를 생성합니다.
public PortBinding(Binding binding, ExposedPort exposedPort)
D. Ports
- **역할:**여러 개의 포트 바인딩(PortBinding) 정보를 관리하기 위한 컨테이너 클래스. 내부적으로 Map<ExposedPort, Binding[]> 형태로 포트 바인딩 정보를 저장
- 주요 메소드:
- bind(ExposedPort, Binding):지정된 ExposedPort와 Binding을 맵에 추가합니다. 이미 존재하면 배열에 추가합니다.
- add(PortBinding... portBindings):여러 PortBinding 객체들을 한 번에 추가합니다.
- getBindings():내부 맵을 반환하여, Docker Remote API와 같은 포맷으로 바인딩 정보를 제공합니다.
- fromPrimitive(...)와 toPrimitive():JSON 직렬화/역직렬화를 위한 헬퍼 메소드입니다.
ExposedPort
- Static Factory 메소드:
- public static ExposedPort tcp(int port)→ 주어진 포트 번호에 대해 TCP 프로토콜의 ExposedPort 인스턴스를 생성합니다.
- public static ExposedPort udp(int port)→ UDP 프로토콜로 ExposedPort 인스턴스를 생성합니다.
- public static ExposedPort sctp(int port)→ SCTP 프로토콜로 ExposedPort 인스턴스를 생성합니다.
- 파싱 기능:
- @JsonCreator public static ExposedPort parse(String serialized)→ Docker CLI에서 사용하는 문자열 형식(예: "80/tcp")을 입력받아, 해당 문자열을 파싱하여 ExposedPort 객체로 변환합니다.
- 문자열이 "80"과 같이 슬래시(/)가 없으면, 기본 생성자를 호출하여 포트 번호만 지정합니다.
- "80/tcp" 형식이면 포트 번호와 프로토콜을 모두 파싱하여 인스턴스를 생성합니다.
- 파싱 중 문제가 발생하면 IllegalArgumentException을 던집니다.
- @JsonCreator public static ExposedPort parse(String serialized)→ Docker CLI에서 사용하는 문자열 형식(예: "80/tcp")을 입력받아, 해당 문자열을 파싱하여 ExposedPort 객체로 변환합니다.
- Getter 메소드:
- public InternetProtocol getProtocol()→ 컨테이너 포트에 사용되는 네트워크 프로토콜을 반환합니다.
- public int getPort()→ 컨테이너에서 노출할 포트 번호를 반환합니다.
- toString() 메소드:
- @JsonValue public String toString()→ 해당 ExposedPort를 문자열 형식으로 표현합니다. 반환 형식은 "포트/프로토콜", 예를 들어 "80/tcp"와 같습니다.
- 이 메소드는 JSON 직렬화 시 사용될 수 있도록 @JsonValue 애너테이션이 붙어 있습니다.
- @JsonValue public String toString()→ 해당 ExposedPort를 문자열 형식으로 표현합니다. 반환 형식은 "포트/프로토콜", 예를 들어 "80/tcp"와 같습니다.
사용예시
// 1. 컨테이너 내부 포트를 정의 (예: SSH나 웹 셸을 위해 22번 포트)
ExposedPort containerPort = ExposedPort.tcp(22);
// 2. Ports 객체를 생성하고, 컨테이너 내부 포트를 호스트의 임의 포트에 매핑 (빈 바인딩 사용)
Ports portBindings = new Ports();
portBindings.bind(containerPort, Binding.empty());
// 3. 컨테이너 생성 명령어 실행: DockerClient를 통해 지정한 이미지로 컨테이너 생성
CreateContainerResponse containerResponse = dockerClient.createContainerCmd(imageName)
.withExposedPorts(containerPort)
.withHostConfig(HostConfig.newHostConfig().withPortBindings(portBindings))
.exec();
// 4. 생성된 컨테이너의 ID를 획득
String containerId = containerResponse.getId();
// 5. 컨테이너 시작
dockerClient.startContainerCmd(containerId).exec();
// 6. 컨테이너의 호스트 포트 매핑 정보를 조회
String hostPort = dockerClient.inspectContainerCmd(containerId)
.exec()
.getNetworkSettings()
.getPorts()
.getBindings()
.get(containerPort)[0]
.getHostPortSpec();
// 컨테이너 내부 22번 포트를 TCP로 노출하는 ExposedPort 생성
ExposedPort containerPort = ExposedPort.tcp(22);
// toString()을 호출하면 "22/tcp"라는 문자열을 반환함
System.out.println(containerPort); // 출력: 22/tcp
ExposedPort parsedPort = ExposedPort.parse("80/tcp");
System.out.println(parsedPort.getPort()); // 출력: 80
System.out.println(parsedPort.getProtocol()); // 출력: TCP (또는 "tcp")
포트 바인딩 코드 작성
1. 포트바인딩 하는 코드 작성
//컨테이너 내부 포트
ExposedPort containerPort = ExposedPort.tcp(22);
//호스트 포트 자동 할당
Ports portBindings = new Ports();
//컨테이너 내부 포트와 호스트 내부 포트 연결
portBindings.bind(containerPort, Ports.Binding.bindPort(0));
HostConfig hostConfig = HostConfig.newHostConfig()
.withPortBindings(portBindings);
아래와 같이 작성해준다.
2. CreateContainerCmd 수정하기(컨테이너 생성 수정)
docker-java/docker-java-api/src/main/java/com/github/dockerjava/api/command/CreateContainerCmd.java at main · docker-java/docke
Java Docker API Client. Contribute to docker-java/docker-java development by creating an account on GitHub.
github.com
이 CreateContainerCmd를 이용해서 컨테이너를 생성해줄 수 있는데,
위의 링크에서 597번째 줄에 아래와 같은 코드가 존재한다.
@CheckForNull
HostConfig getHostConfig();
CreateContainerCmd withHostConfig(HostConfig hostConfig);
이 코드로부터 우리는 포트 바인딩 값을 넣어서 생성을 해 줄 수가 있고,
(hostConfig과 관련된 내용은 https://github.com/docker-java/docker-java/blob/main/docker-java-api/src/main/java/com/github/dockerjava/api/model/HostConfig.java#L22 여기서 확인)
docker-java/docker-java-api/src/main/java/com/github/dockerjava/api/model/HostConfig.java at main · docker-java/docker-java
Java Docker API Client. Contribute to docker-java/docker-java development by creating an account on GitHub.
github.com
public HostConfig withPortBindings(PortBinding... portBindings) {
requireNonNull(portBindings, "portBindings was not specified");
withPortBindings(new Ports(portBindings));
return this;
}
hostConfig의 이 코드를 토대로, 위에서 컨테이너 내부 22번 포트와 외부 랜덤 포트를 연결해주었으니, 그것으로 연결을 해줄 수가 있다.
그렇기에 생성하는 코드를 작성하면
// ** 컨테이너 생성 및 포트 바인딩** //
CreateContainerResponse containerResponse = dockerClient.createContainerCmd(imageName)
.withExposedPorts(containerPort)
.withHostConfig(hostConfig)
.exec();
이렇게 작성을 해줄 수가 있다.
3. 포트바인딩 유지를 위해 컨테이너 실행하기
위 코드 그대로 냅두고 실행을 하면, 컨테이너 생성은 된다. 근데 그게 끝이다. 저대로 실행하면
이런 오류가 발생하게 된다.
왜냐?
첫 번째로 실행을 시켜주지 않았기에 도커의 상태는
Created에 멈춰있는 것
내부 포트가 열려야(22번 포트) 외부 포트랑도 연결이 될 테니 저 포트바인딩 코드가 실현되기 위해서는 컨테이너가 실행이 되어야 한다.
컨테이너 실행은
docker-java/docker-java-api/src/main/java/com/github/dockerjava/api/DockerClient.java at main · docker-java/docker-java
Java Docker API Client. Contribute to docker-java/docker-java development by creating an account on GitHub.
github.com
174번줄의
StartContainerCmd startContainerCmd(@Nonnull String containerId);
를 이용하면 된다.
그러기에 아래와 같이 코드를 작성해준다.
//컨테이너 아이디 가져오기
String containerId = containerResponse.getId();
System.out.println("컨테이너 생성 완료: " + containerId);
// 컨테이너 실행
dockerClient.startContainerCmd(containerId).exec();
System.out.println("컨테이너 실행 완료" + containerId);
4. .withCmd 작성
저대로 까지 했으면 되지 않을까...인데 여전히
오류가 발생한다.
분명 컨테이너는 실행이 완료 되었는데 말이다.
이거 원인찾느라 굉장히 오래걸렸는데 원인을 알았다.
맨 위의 줄, /bin/bash 가 COMMAND인데, 상대를 보면, Exited(0)으로 적혀있다. 즉 정상적으로 종료가 되었다는 의미이다. 저 프로세스가 끝나지 않게 유지 시켜줘야하는 것이다. 왜냐? 시작하자마자 정상 종료가 되었으니까 내부 포트 22번이 죽어버리지...
그래서
아까 생성코드에다가
// ** 컨테이너 생성 및 포트 바인딩** //
CreateContainerResponse containerResponse = dockerClient.createContainerCmd(imageName)
.withCmd("tail", "-f", "/dev/null")
.withExposedPorts(containerPort)
.withHostConfig(hostConfig)
.exec();
이렇게 .withCmd를 해주면 정상적으로 작동한다.
InspectContainerCmd르 바인딩된 포트 조회하기
1. InspectComtainCmd 자세히 알아보기
아까 위에서 랜덤으로 비어있는 포트 하나 랜덤으로 만들어서 거기에 바인딩을 했었는데, 저번 DTO에서는 해당 포트를 조회해야 한다.
docker-java/docker-java-api/src/main/java/com/github/dockerjava/api/command/InspectContainerCmd.java at main · docker-java/dock
Java Docker API Client. Contribute to docker-java/docker-java development by creating an account on GitHub.
github.com
여기서
public interface InspectContainerCmd extends SyncDockerCmd<InspectContainerResponse> {
@CheckForNull
String getContainerId();
InspectContainerCmd withContainerId(@Nonnull String containerId);
이 부분을 활용하여 containerId를 넣어서 해당 컨테이너를 면밀히 살펴볼 수 있다.
@Override
InspectContainerResponse exec() throws NotFoundException;
interface Exec extends DockerCmdSyncExec<InspectContainerCmd, InspectContainerResponse> {
}
}
를 보면 InspectContainerResponse 를 반환하는 것을 확인할 수 있다.
docker-java/docker-java-api/src/main/java/com/github/dockerjava/api/command/InspectContainerResponse.java at main · docker-java
Java Docker API Client. Contribute to docker-java/docker-java development by creating an account on GitHub.
github.com
여기서 어떤 응답들을 리턴하는 지 확인할 수 있고
hostconfig도 보이고, 밑으로 내리다 보면 NetworkSettings도 보인다.
2. NetWorkSettings에서 바인딩 정보 가져오는 과정
나는 처음에 솔직히 HostConfig에 바인딩된 정보 있을 줄 알고 저기 뒤져봤는데 안 되어서 시간 좀 많이 버렸다.
(이 문제는 지피티 형님이 해결해주셨다.)
속성 | HostConfig | NetworkSettings |
역할 | 컨테이너의 호스트 관련 설정 (볼륨, 제한 설정 등) | 컨테이너의 네트워크 관련 설정 (IP, 포트 바인딩 등) |
포함 정보 | cpu 제한, 메모리 제한, 볼륨 설정, restart policy 등 | IP 주소, 게이트웨이, 포트 바인딩 등 |
포트 바인딩 정보 | 포함되지 않음 ❌ | 포함됨 ✅ |
사용 예시 | 컨테이너의 자원 제한을 설정할 때 | 컨테이너가 어떤 네트워크에 속하고, 어떤 포트가 바인딩되었는지 확인할 때 |
그렇기에 Hostconfig가 아닌, NetworkSettings에 넘어가면 바인딩된 정보를 가져올 수 있다.
docker-java/docker-java-api/src/main/java/com/github/dockerjava/api/model/NetworkSettings.java at main · docker-java/docker-jav
Java Docker API Client. Contribute to docker-java/docker-java development by creating an account on GitHub.
github.com
NetworkSEttings의 119번째줄에는
public Ports getPorts() {
return ports;
}
이 코드가 있고,
docker-java/docker-java-api/src/main/java/com/github/dockerjava/api/model/Ports.java at main · docker-java/docker-java
Java Docker API Client. Contribute to docker-java/docker-java development by creating an account on GitHub.
github.com
저 Ports내부를 까보면 86번째 줄에
public Map<ExposedPort, Binding[]> getBindings() {
return ports;
}
이를 통해서 포트객체에서 포트바인딩된 정보를 담은 맵을 직접 반환하고, ExposedPort키로 해당 포트에 바인딩된 배열을 가져온다.
저 결과를
/**
* @return the port spec for the binding on the Docker host. May reference a single port ("1234"), a port range ("1234-2345") or
* <code>null</code>, in which case Docker will dynamically assign a port.
*/
public String getHostPortSpec() {
return hostPortSpec;
}
이 메서드를 통해서 할당된 포트번호를 문자열로 반환해준다.
3. 코드 작성
이를 토대로 코드를 작성하면
String hostPort = dockerClient.inspectContainerCmd(containerId)
.exec() // InspectContainerResponse 반환
.getNetworkSettings() // 컨테이너의 네트워크 설정 가져오기
.getPorts() // 네트워크 설정에서 Ports 객체 가져오기
.getBindings() // Map<ExposedPort, Binding[]> 형태로 포트 바인딩 정보 가져오기
.get(containerPort)[0] // 특정 ExposedPort (예: 22/tcp)에 대한 첫 번째 Binding 선택
.getHostPortSpec(); // 선택된 Binding에서 호스트에 할당된 포트 번호를 문자열로 반환
//컨테이너 아이디, 포트번호 return
ContainerInfoDto containerInfoDto = ContainerInfoDto.builder()
.containerId(containerId)
.port(hostPort)
.build();
return containerInfoDto;
이렇게 작성을 할 수 있다.
데이터베이스에 값 저장하기
이제 마지막 파트이다. 여기서부터는 실질적으로 기본 스프링 하는 거랑 비슷하니 빠르게 넘어가겠다.
1. Container 작성
package com.hanbat.dotcar.container;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.Date;
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Entity
@Builder
public class Container {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String containerId; //컨테이너 아이디
private String containerName; // 컨테이너 이름
@Column(nullable = false)
private String os; //운영체제
private String version; //버전
private Date createdAt; //컨테이너 생성 시간
private String status; // 컨테이너 상태
private String hostPort; // 컨테이너가 매핑된 호스트 포트 번호
private String madeBy; //생성한 사람의 이메일
}
2. ContainerRepository 작성
package com.hanbat.dotcar.container;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface ContainerRepository extends JpaRepository<Container, Long> {
Optional<Container> findByContainerId(String ContainerId);
Optional<Container> findByMadeBy(String MadeBy);
}
findContainerId 는 나중을 위해서, findByMadeBy는 나중을 위한 큰그림
3. ContainerService 전체 코드
package com.hanbat.dotcar.container;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.command.CreateContainerResponse;
import com.github.dockerjava.api.command.PullImageResultCallback;
import com.github.dockerjava.api.exception.NotFoundException;
import com.github.dockerjava.api.model.ExposedPort;
import com.github.dockerjava.api.model.HostConfig;
import com.github.dockerjava.api.model.Ports;
import com.github.dockerjava.core.DefaultDockerClientConfig;
import com.github.dockerjava.core.DockerClientConfig;
import com.github.dockerjava.core.DockerClientImpl;
import com.github.dockerjava.transport.DockerHttpClient;
import com.github.dockerjava.zerodep.ZerodepDockerHttpClient;
import lombok.RequiredArgsConstructor;
import org.apache.commons.io.LineIterator;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;
import javax.print.Doc;
import java.time.Duration;
import java.util.Date;
@RequiredArgsConstructor
@Service
public class ContainerService {
private final ContainerRepository containerRepository;
/**************************/
/** 공식 문서 사용 도커 연결 **/
// DockerClientConfig 인스턴스 생성 -> Docker 데몬에 접근할 수 있도록 환경 설정(예: DOCKER_HOST, 인증 관련 정보 등)을 제공하는 객체를 생성
private final DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder()
// .withDockerHost("unix:///var/run/docker.sock")
.build();
private final DockerHttpClient httpClient = new ZerodepDockerHttpClient.Builder()
.dockerHost(config.getDockerHost()) // DockerClientConfig에서 Docker 호스트 정보 가져오기
.sslConfig(config.getSSLConfig()) // SSL 구성 (TLS 인증서 등)
.maxConnections(100) // 최대 연결 수 설정
.connectionTimeout(Duration.ofSeconds(30)) // 연결 타임아웃 설정
.responseTimeout(Duration.ofSeconds(45)) // 응답 타임아웃 설정
.build();
// DockerClient 인스턴스 생성 -> DockerClientConfig와 DockerHttpClient를 결합하여 Docker 데몬에 명령을 전달할 수 있는 DockerClient 객체를 생성
private final DockerClient dockerClient = DockerClientImpl.getInstance(config, httpClient);
/**************************/
/**************************/
/****** 컨테이너 생성 ********/
/**************************/
public ContainerInfoDto createContainer(CreateContainerRequestDto createContainerRequestDto){
//TODO : 이메일 검증
String userEmail = createContainerRequestDto.getUserEmail();
//TODO : 운영체제별, 버전별로 나누기 -> 추후 예정
//Version이 비어있을 경우
String os = createContainerRequestDto.getOs();
String version = (createContainerRequestDto.getVersion()) == null || createContainerRequestDto.getVersion().isEmpty()
? "latest" : createContainerRequestDto.getVersion();
//TODO : 후에 도커 이미지를 따로 경량화 시켰을 경우 해당 이미지로 변경
String imageName = os + ":" + version;
//이미지 존재 여부 확인하고, 없으면 다운로드
try {
dockerClient.inspectImageCmd(imageName).exec();
} catch (NotFoundException e) {
System.out.println("이미지가 없어서 다운로드 중...");
try {
dockerClient.pullImageCmd(os)
.withTag(version)
.exec(new PullImageResultCallback())
.awaitCompletion();
} catch (Exception ex) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "Docker 이미지 다운로드 실패", ex);
}
}
// ** 포트 바인딩 ** //
//컨테이너 내부 포트
ExposedPort containerPort = ExposedPort.tcp(22);
//호스트 포트 자동 할당
Ports portBindings = new Ports();
//컨테이너 내부 포트와 호스트 내부 포트 연결
portBindings.bind(containerPort, Ports.Binding.bindPort(0));
HostConfig hostConfig = HostConfig.newHostConfig()
.withPortBindings(portBindings);
// ** 컨테이너 생성 및 포트 바인딩** //
CreateContainerResponse containerResponse = dockerClient.createContainerCmd(imageName)
.withCmd("tail", "-f", "/dev/null")
.withExposedPorts(containerPort)
.withHostConfig(hostConfig)
.exec();
//컨테이너 아이디 가져오기
String containerId = containerResponse.getId();
System.out.println("컨테이너 생성 완료: " + containerId);
// 컨테이너 실행
dockerClient.startContainerCmd(containerId).exec();
System.out.println("컨테이너 실행 완료" + containerId);
String hostPort = dockerClient.inspectContainerCmd(containerId)
.exec() // InspectContainerResponse 반환
.getNetworkSettings() // 컨테이너의 네트워크 설정 가져오기
.getPorts() // 네트워크 설정에서 Ports 객체 가져오기
.getBindings() // Map<ExposedPort, Binding[]> 형태로 포트 바인딩 정보 가져오기
.get(containerPort)[0] // 특정 ExposedPort (예: 22/tcp)에 대한 첫 번째 Binding 선택
.getHostPortSpec(); // 선택된 Binding에서 호스트에 할당된 포트 번호를 문자열로 반환
// ** 데이터베이스에 저장 ** //
String containerName = dockerClient.inspectContainerCmd(containerId).exec().getName();
String status = dockerClient.inspectContainerCmd(containerId).exec().getState().getStatus();
Container container = Container.builder()
.containerId(containerId)
.containerName(containerName)
.os(os)
.version(version)
.createdAt(new Date())
.status(status)
.hostPort(hostPort)
.madeBy(userEmail)
.build();
containerRepository.save(container);
System.out.println("컨테이너 저장" + containerId);
//컨테이너 아이디, 포트번호 return
ContainerInfoDto containerInfoDto = ContainerInfoDto.builder()
.containerId(containerId)
.port(hostPort)
.build();
return containerInfoDto;
//TODO : 예외 처리
}
}
이때 데이터베이스에 containerName과 status가 들어가기에
String containerName = dockerClient.inspectContainerCmd(containerId).exec().getName();
String status = dockerClient.inspectContainerCmd(containerId).exec().getState().getStatus();
이를 통해서 가져왔다.
(앞으로 설명에 저렇게 타고타고 들어가는 것 보단 그냥 이제부터 나도 지피티 돌려서 저런 게 있구나 하고 사용할 것 같다.)
4. ContainerController작성
저번 코드 그대로 사용한다
package com.hanbat.dotcar.container;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@RequestMapping("api/container")
public class ContainerController {
private final ContainerService containerService;
@PostMapping("/create")
public ResponseEntity<CreateContainerResponseDto> createContainer(@RequestBody CreateContainerRequestDto createContainerRequestDto){
ContainerInfoDto containerInfoDto = containerService.createContainer(createContainerRequestDto);
//TODO : 컨테이너 생성 실패 시에 나오는 예외처리
CreateContainerResponseDto createContainerResponseDto = CreateContainerResponseDto.builder()
.containerId(containerInfoDto.getContainerId())
.port(containerInfoDto.getPort())
.build();
return ResponseEntity.status(200).body(createContainerResponseDto);
}
}
테스트
이미지와
아무것도 없는 컨테이너인 상태에서
실행을 하면
이미지를 다운받고 컨테이너를 새로 만든다.
포트번호까지 제대로 나온다.
docker ps -a
로
포트 맞는지도 확인한다.
데이터베이스도 되는지
확인하면 끝!
잘 수행되었다.
에필로그
3편에서는 추가적인 생성시에 필요한 디테일을 작성할텐데, 내가 만들려는 프로젝트에 맞게 수정할 것이기에 따로 docker-java를 건들지는 않을테니 , 다음글은 따로 보지 않아도 된다.(물론 나도 작성 안 해도 됨)
읽어주느라 모두 고생하셨고 감사합니다.
이거 ㄹㅇ 지피티의 도움이 많이 필요할 것 같다. 공식문서를 내가 못찾아서 코드 하나하나 올라가면서 찾는데 너무 비효율적이라서 이제부터 100% 지피티를 신뢰하도록 해야할 것 같다.
'웹 개발' 카테고리의 다른 글
[SpringBoot] docker-java 사용하여 스프링부트로 도커 조작하기 - 컨테이너 생성(1/3) (0) | 2025.02.13 |
---|---|
[SpringBoot] docker-java 사용하여 스프링부트로 도커 조작하기 - 사전준비 (0) | 2025.02.13 |
[장고] 회원가입 기능 상세하게 마무리 (이메일 인증) (0) | 2024.03.08 |
[장고] 회원가입시 이메일 인증하기(이메일 유효성 검사) #5 (1) | 2024.02.11 |
[장고] JWT 사용하기 #4 (0) | 2024.02.10 |