본문 바로가기

프로그래밍/Docker

docker에 대한 형식없는 정리

docker 공부하면서 case by case로 체계없이 정리한 것들을 기록한 것들이다.

이 내용들은 늘어나거나 줄어들거나 수정될 수 있다는 것을 미리 말해둔다

말그대로 docker를 공부하면서 알게된 단편 지식들을 보관 차원에서 적어둔 것이기 때문이다

 

1. docker hub에서 이미지를 받을때는 pull 명령을 사용한다. 예를 들어 wildfly의 최신버전(latest)를 받을 경우 다음과 같이 한다.

 

docker pull jboss/wildfly:latest

 

2. 내가 받은 이미지들의 목록을 볼 경우 다음과 같이 한다

 

docker images

 

3. 이미지 세부 정보를 확인할 때는 inspect 명령어를 사용한다.

 

docker inspect jboss/wildfly:latest

 

세부 정보는 json 문자열 형태로 보여지며 주요 정보로는 이미지 ID, 생성일, Docker 버전, 이미지 생성자, CPU 등을 제공한다

정보가 json 문자열 형태로 길게 나오기때문에 만약 특정 정보를 보고자 할때는 다음과 같이 format 옵션을 주어 보고자 하는 항목을 지정하면 그 항목에 대해서만 볼 수 있다. 예를 들어

 

docker inspect --format="{{ .Os}}" jboss/wildfly:latest

 

를 사용하면 Os 항목에 대한 값만 알 수 있다. 또 Config 항목의 하위 항목으로 있는 Hostname 항목 값을 알고자 할때는 다음과 같이 한다

 

docker inspect --format="{{ .Config.Hostname}}" jboss/wildfly:latest

 

이것은 우리가 json 문자열을 객체로 만들었을때 접근하는 방법을 생각하며 이 명령을 사용하면 된다

 

4. 이미지를 삭제할때는 다음과 같이 한다

 

docker rmi jboss/wildfly:latest

 

5. docker 이미지를 받았으면 이제 이 이미지를 가지고 하나의 컨테이너를 생성해서 실행시키도록 한다. 이해하기 쉽게끔 설명하자면 프로그래밍에서 얘기하는 클래스와 객체로 설명하면 이해하기 쉬울듯 하다.

 

A라는 클래스를 이용해서 객체를 만들때..

 

A b = new A();

A c = new A();

 

이렇게 만들것이다. A라는 클래스를 이용해서 만든 객체 b와 c가 있지만 이 b와 c는 서로를 간섭하지는 않는다. 이것을 docker에 비춰보면 클래스는 이미지가 되고 객체는 컨테이너가 된다. 필요성에 따라 이미지를 이용해서 0개 이상의 컨테이너를 만들수 있지만 이렇게 만들어진 컨테이너들은 서로 독립적인 공간이 되어 서로 간섭하지를 않게 된다

 

컨테이너의 라이프 사이클은 docker 명령어를 빌려서 설명을 하자면 create(생성) -> start(시작) -> stop(중지) -> rm(삭제) 개념으로 흘러가며 create와 start를 같이 해주는 명령어인 run이 있고 start인 상태에서 중지했다가 다시 시작하는 명령어로 restart가 있다

 

6. docker에서 컨테이너를 생성하면서 동시에 시작할때는 run을 사용한다. wildfly를 예로 들자면 다음과 같이 한다

 

docker run -it --name "wildfly" -p 8080:8080 jboss/wildfly:latest

 

run 명령어를 사용하면서 같이 사용된 옵션에 대해 설명을 하자면 

 

-i 옵션은 컨테이너 표준 입력을 연다는 뜻이고, -t는 tty(단말 디바이스)를 사용한다는 의미이다. 이 둘을 묶어서 -it로 사용한 것이다. 

 

--name 옵션을 주면 해당 컨테이너에 대해 특별한 이름을 부여하는 옵션이다. 컨테이너를 start 명령어로 시작하거나 stop 명령어를 이용해서 종료할때 대상 컨테이너를 지정하기 위해 컨테이너 ID를 입력해야 하는데 이 ID라는게 의미가 없는 숫자/문자가 조합된 문자열이라 타이핑하기 어렵다. 이러한 상황을 좀더 쉽게 사용하게 하기 위해 --name 옵션을 주어 의미있는 문자열을 지정함으로써 차후에 start, stop 명령어를 사용할때 그 문자열을 사용하여 해당 명령어를 쉽게 실행할 수 있다.

 

-p는 호스트와 컨테이너 간의 포트를 연결하는 옵션이다. wildfly는 8080 포트를 이용해서 통신하게 되는데 이것은 어디까지나 컨테이너의 8080 포트를 열은 것이기 때문에 우리가 테스트하기위해 브라우저에서 http://localhost:8080을 입력해도 동작이 이루어지지 않는다. 이로 인해 호스트의 8080 포트를 컨테이너의 8080 포트와 연결해야 제대로 된 동작을 할 수 있다.  한가지 더 첨언을 하자면 컨테이너가 8080 포트를 열었다고 해서 호스트도 반드시 8080 포트를 열으라는 법은 없다. 예를 들어 wildfly 컨테이너를 2개 이상 운영하는데 두 컨테이너가 모두 8080 포트를 열어도 문제가 되는 것은 없다. 그러나 이 두 포트를 모두 호스트의 8080 포트에 연결할 수는 없다. 즉 둘 중 하나의 컨테이너는 -p 8081:8080 이런 식으로 호스트의 8081 포트를 컨테이너의 8080 포트로 열어서 동작하게끔 해야한다

 

7. 6번의 과정으로 실행을 하게 되면 docker의 foreground로 실행하는 것이기 때문에 이런 식으로의 실행은 추천하지 않는다.  즉 다른 컨테이너를 시작할려면 다시 쉘 창을 열어서 docker 관련 명령을 실행해야 하기 때문이다. 대신 foreground에서 실행시키는 것이기 때문에 컨테이너를 중지할려면 ctrl-c를 눌러 중지할 수 있으며 관련 콘솔 로그 내용도 바로 확인 할 수 있는 장점은 있다. 그러나 docker로 실행되는 것이 WAS나 DB 서버 같은 서버용 프로그램인것을 감안하면 background로 실행되는 것이 좋다

 

8. 7번에서 설명한대로 background로 컨테이너가 실행되게끔 할려면 -d 옵션을 붙이면 된다

 

docker run -d --name "wildfly" -p 8080:8080 jboss/wildfly:latest 

 

6번에서 설명했을때는 -it옵션을 붙였으나 여기서는 background로 실행하는 것이기 때문에 컨테이너 표준입력을 사용할 수가 없다. 그걸 제외한 나머지 옵션은 동일하다

그러나 이렇게 실행하면 6번에서 컨테이너가 보여주는 로그들을 볼 수 없는 문제가 있다. 이를 볼려면 logs 명령어를 사용하면 된다

 

docker logs -f wildfly

 

-f 옵션은 linux 에서 tail 명령 사용했을때 -f 사용한 것을 생각하면 된다. 즉 로그를 계속 연속성으로 보여주는 것으로 이해하면 된다. 뒤에 wildfly가 붙은 것은 우리가 docker run을 이용해서 컨테이너를 실행할때 --name 옵션을 이용해서 주었던 그 wildfly이다. 이렇게 컨테이너를 run 명령어를 이용해서 실행할때 --name 옵션으로 별칭 준것을 docker 관련 명령 실행시 특정 컨테이너를 지칭하는데 사용함으로써 편리함을 준다.

 

9. 처음에 docker를 접하면서 난 docker의 이미지가 프로그램들의 모음(예를 들어 wildfly 컨테이너를 예로 들자면 wildfly와 이를 실행시키기 위해 필요한 부가적인 프로그램(예를 들면 jdk 같은)들의 모음)으로 이해했다. 그러나 docker 관련 구성도를 보면서 이해하게 된것은 컨테이너 안에는 os가 있다는 것이다. 즉 os 안에 관련 프로그램이 설치되어 있는 개념인 것이다. 이 부분은 나중에 build를 공부하면서 확연히 알게 되었다. 즉 기본 베이스 이미지를 centos 같은 리눅스 이미지로 삼은뒤 여기에 apache를 yum 명령어로 설치하면서 이미지 만드는 법을 알게 되었다 물론 베이스 이미지를 centos 같은 os 이미지가 아니라 wildfly 이미지를 사용할 수 있다. 그렇다해도 wildfly에 centos가 이미 있기 때문에 결국 wildfly 이미지에 있는 centos 를 이용하는 것임에는 변함이 없다. 이러한 개념이기 때문에 해당 컨테이너는 독자적인 OS와 그에 따른 환경변수를 가지고 실행할 수가 있는것이며 서로간의 컨테이너에 영향을 주지 않고 독립적으로 움직이게 되는 것이다.

 

docker 이미지는 os가 포함이 되어 있는 0개 이상의 프로그램이 설치되어 있는 이미지이다

 

10. 9번에서 설명했다시피 이미지는 자체적으로 OS가 포함되어 있다고 했다. 그렇기 때문에 우리는 컨테이너가 실행중일때 이 컨테이너를 구동시키는 기반이 되는 OS에 접속할 수가 있다. 즉 우리가 wildfly 이미지를 이용하여 컨테이너를 실행중일때 이 컨테이너의 OS에 연결할 수 있다는 것이다. 이것은 나름 작업(?)적인 차원에서 중요한 의미가 있다. 예를 들어 우리가 wildfly에서 구동중인 web application을 만든다고 가정해보자. 그러면 파일 업로드 기능을 넣어야 하는데 파일 업로드를 하게 되면 OS에서 관리되는 디렉토리 중 하나에 사용자가 업로드한 파일이 있어야 하며 그러한 디렉토리를 만들어놔야 한다. 그러면 이 디렉토리를 만들려면 wildfly 컨테이너의 os 쉘에 접근해서 디렉토리를 만드는 명령(mkdir)을 실행해야 한다. 이러한 OS 쪽의 작업을 하거나 또는 wildfly의 설정파일(예를 들면 standalone.xml)을 수정할 수 있다. 그리고 이렇게 변경이 된 내용이 있는 컨테이너를 docker commit 명령을 이용해서 새로운 이미지로 만들어 이를 다른 사람에게 배포할 수 있다.

 

서론이 길었는데 이 기능을 수행하는것에 대해 설명하도록 하겠다

먼저 경우를 분리해서 생각해야 할 부분이 있는데 우리가 컨테이너를 실행시킬때 /bin/bash 같은 쉘을 실행시킨 상황과 그렇지 않은 상황으로 분리해야한다. /bin/bash를 실행시켜서 현재 컨테이너가 bash shell이 실행중인 상황이면 docker의 attach 명령을 이용해서 접속하면 된다

 

docker attach wildfly

 

이러면 bash shell 에 접속하여 shell 명령어를 실행시킬수 있다. 여기서는 컨테이너 이름을 지칭하기 위해 wildfly를 썼지만 실제로 wildfly는 이렇게 명령해도 접속이 되질 않는다. 왜냐면 wildfly는 bash shell을 실행시키지 않고 컨테이너를 띄우기 때문이다. 때문에 attach 명령으로 접속 시도를 해도 동작이 되질 않는다. 또한 백그라운드로 실행시킨 컨테이너의 경우도 마찬가지이다(왜 그런지는 곰곰히 생각해보면 이해하기 쉽다. 우리가 background로 컨테이너를 실행시킨다는건 docker 명령어를 실제로 내리는 shell 창에서 억세스하는 것이 아니라 말그대로 background로 돌리는 것인데 거기서 shell 명령을 실행시키는 /bin/bash가 실행될리가 없잖은가?) 이렇게 /bin/bash 를 실행시키지 않고 시작한 컨테이너나 또는 background에서 실행중인 컨테이너를 접속하기 위해서는 exec 명령을 사용해서 /bin/bash를 실행시켜 접속하면 된다

 

docker exec -it -u root wildfly /bin/bash

 

이렇게 exec 명령을 통해 /bin/bash를 실행시켜서 bash shell 명령을 실행시킬수가 있게 된다. -u는 os의 어떤 계정을 통해서 명령을 실행시킬것인지를 지정하는 것이다. 그러면 어떤 계정을 사용해야 하는가? 그것은 해당 컨테이너의 docker 이미지를 build 하는데 사용된 Dockerfile의 소스를 보면 알 수 있다. 

 

일반적으로 docker 이미지들은 github 에서 이미지 관련 파일들이 존재하여 이를 github에서 빌드하여 docker hub에 배포되는 형태를 취하고 있다. 그래서 이 Dockerfile의 소스를 볼려면 github을 봐야한다. wildfly를 예로 들면 docker hub의 wildfly에 대한 페이지(https://hub.docker.com/r/jboss/wildfly/)를 가보면 Source 항목에 github과 링크되어 있는 링크를 볼 수 있다. 이를 클릭하면 github에 있는 해당 이미지를 빌드하는데 사용된 Dockerfile을 볼 수 있게 된다. 이 Dockerfile 을 클릭해서 상세 내용을 보면 USER 란 항목이 있어 Dockerfile을 이용해 빌드하면서 실행하게 되는 명령어를 수행하는 OS 계정을 언급하게 된다. 바로 이 USER 항목에 언급된 계정을 -u 옵션 뒤에 사용해주면 된다.  모든 docker 이미지가 root 계정을 사용하는 것은 아니기 때문에 이것을 확인하고 진행하기 바란다.

 

attach와 exec에 대한 추가 설명

위의 내용을 읽어보면 bash shell 기능을 위주로 설명했기때문에 현재 bash shell이 실행중인 컨테이너를 접속하여 할때는 attach, bash shell 이 실행중이지 않은 컨테이너를 접속하려 할때는 exec를 사용하는 것으로 이해가 될 것 같아서 부가설명을 하고자 한다.

 

attach는 현재 실행중인 컨테이너에 접속 한다고 보는 것이 맞다. docker 이미지가 컨테이너로 생성되면서 run이 될때 이미지 가 내부적으로 특정 프로그램을 실행시킬수도 있고 또는 run 명령을 내리는 시점에서 특정 프로그램을 실행 시키게도 할 수 있다. 이때 bash shell 이 실행되도록 /bin/bash 를 실행시켜서 attach 시 바로 bash shell prompt가 보이게끔 할 수도 있다. 그러나 그렇지 않은 이미지도 있다. 예를 들어 위에서 언급했던 wildfly 이미지의 경우 이 이미지는 컨테이너로 올라가는 시점에 wildfly의 standalone.sh 스크립트가 실행시켜 wildfly가 구동되도록 하고 있기 때문에 콘솔 화면에서 wildfly의 로그가 올라오는 것을 볼 수 있다. 그래서 docker의 attach 명령을 이용해서 wildfly의 컨테이너를 접속하면 wildfly 구동되고 있는 상황에서 나오는 콘솔로그를 볼 수 있다. 또한 이 상태에서 ctrl-c를 클릭하면 wildfly 가 실행되는 것이 종료되는 것을 볼 수 있다.

 

이에 반해 exec는 현재 실행중인 컨테이너에 특정 shell script를 실행하는 것이라고 보는게 맞다. 예를 들어 wildfly 이미지의 컨테이너가 wildfly란 이름으로 컨테이너가 실행되어 있는 상태에서 docker exec -it wildfly /bin/bash 로 명령을 내리면 해당 컨테이너에 bash shell을 실행시키는 것이다. 그래서 bash shell prompt가 화면에 나오게 되는 것이다. 만약 /bin/bash가 아는 사용자가 만든 별도 스크립트를 실행시키면 그 스크립트가 실행시키는 것이다(exec 명령어를 사용할때 -it 옵션을 주었기 때문에 해당 명령의 실행 결과가 현재 보는 화면에 바로 보이게 된다)

 

11. docker 에서 등록되어 있는 컨테이너 목록을 확인 하는 명령은 다음과 같이 한다

 

docker ps -a 

 

-a 옵션을 빼고 docker ps 로 실행하게 되면 현재 동작중인 컨테이너만 보여주기 때문에 중지되어 있는 컨테이너는 보여주지 않게 된다. 그래서 -a 옵션을 항상 붙여서 중지되어 있는 것도 같이 확인하는 버릇(?)을 들이는 것이 좋다

 

12.  docker에서 실행중인 각 컨테이너가 사용하는 CPU 및 메모리 점유율 등 컨테이너의 상태를 확인하기 위해서는 다음의 명령을 사용한다

 

docker stats

 

13. 중지되어 있는 컨테이너를 구동하는 명령은 다음과 같이 한다

 

docker start wildfly

 

14. 실행중인 컨테이너를 중지시키는 명령은 다음과 같이 한다.

 

docker stop wildfly

 

15. 컨테이너를 재시작할때의 명령은 다음과 같이 한다

 

docker restart wildfly

 

16. 컨테이너를 삭제할때의 명령은 다음과 같이 한다

 

docker rm -f wildfly

 

원래 컨테이너를 삭제할때는 현재 상태가 중지되어 있는 컨테이너만 삭제가 가능하기 때문에 실행중인 컨테이너는 삭제를 할 수가 없다. 그러나 -f 옵션을 붙이면 실행중인 컨테이너도 삭제하게 된다

현재 컨테이너에 등록되어 있는 모든 컨테이너를 삭제하고자 할 때는 다음과 같이 한다

 

docker rm $(docker ps -a -q)

 

여기에 위에서 설명한 -f 옵션을 붙이면 실행중인 컨테이너까지 모두 삭제하게 된다

 

17. 현재 구동중인 컨테이너에서 실행 중인 프로세스를 모두 일시정지 시키고자 할 때는 다음과 같이 한다

 

docker pause wildfly

 

일시정지된 컨테이너를 docker ps 명령어를 이용해서 컨테이너 목록으로 확인해보면 Status에 Paused로 나온다(참고로 stop 명령을 이용해서 컨테이너를 중지시킨 경우 docker ps 명령을 이용해 목록을 확인해보면 Status에 Exited로 나온다)

이렇게 일시정지된 컨테이너를 다시 재시작 시킬때는 다음과 같이 한다

 

docker unpause wildfly

 

18. 현재 구동중인 컨테이너에서 실행중인 프로세스를 확인할때는 다음과 같이 한다

 

docker top wildfly

 

linux에서 top 명령을 실행한것과 같은 유형의 결과물을 보여준다

 

19. 구동중인 컨테이너에서 실행중인 프로세스가 이용중인 포트를 확인하고자 할때는 다음과 같이 한다

 

docker port wildfly

 

이렇게 하면 우리가 위에서 실행한 wildfly의 경우 다음과 같이 나타날 것이다

 

8080/tcp ->0.0.0.0:8080

 

이것은 컨테이너의 8080 포트(왼쪽의 8080/tcp)를 호스트 컴퓨터(정확하게는 현재 내가 사용중인 OS)의 8080 포트(오른쪽의 0:.0.0.0:8080)와 연결되고 있음을 의미한다.

 

20. 컨테이너와 호스트 컴퓨터 간의 파일 복사는 다음과 같이 한다

 

docker cp wildfly:/opt/jboss/wildfly/standalone/configuration/standalone.xml D:/dockerimages

 

위의 명령은 wildfly 컨테이너의 /opt/jboss/wildfly/standalone/configuration 디렉토리에 있는 standalone.xml 파일을 호스트 컴퓨터의 D:/dockerimages  디렉토리에 복사한다는 뜻이다. 

명령어의 구조가 docker cp 복사대상파일 복사되는위치 구조이기 때문에 이 구조만 맞춰주면 마찬가지로 호스트에서 컨테이너로 파일 복사를 할 수 있다. 위에서 언급한 명령을 호스트에서 컨테이너로 복사하는 개념으로 바꿔서 한다면 다음과 같이 한다

 

docker cp D:/dockerimages/standalone.xml wildfly:/opt/jboss/wildfly/standalone/configuration

 

이렇게 하면 호스트 컴퓨터의 D:/dockerimages/standalone.xml 파일을 wildfly 컨테이너의 /opt/jboss/wildfly/standalone/configuration 디렉토리에 복사하게 된다. 명령어를 유심히 보면 알겠지만 컨테이너의 디렉토리나 디렉토리에 있는 파일을 언급할때는 컨테이너 이름과 컨테이너의 디렉토리 또는 파일 사이에 :(콜론)을 붙인다(wildfly:/opt/jboss/wildfly/standalone/configuration/standalone.xml)

 

21. 기본적으로 Docker에서 실행되는 명령은 OS의 root 계정으로 실행이 된다(그도 그럴것이 Dockerfile에서 ENV를 이용해서 OS의 환경변수도 설정이 가능하기 때문에 이런것이 가능할려면 root 계정일수 밖에 없다) 그러나 root 계정이 아닌 다른 계정을 만들어서 명령을 실행시켜야 할 수도 있다. 이럴땐 Dockerfile에서 해당 계정을 만든뒤 USER 명령을 이용해서 해당 계정으로 바꾼다. 그러면 그 후에 실행되는 것은 해당 계정으로 실행하는 것이 된다. 

 

22. 현재 실행중인 컨테이너를 이미지로 만들어야 할 수 있다. 이미지로 제공되는 설정을 컨테이너로 실행되게 한 상태에서 해당 설정을 바꾸고 이를 이미지로 저장해서 재사용할 수도 있기 때문이다. 이럴때는 commit 명령을 이용한다. 예를 들어 wildfly란 이름의 컨테이너를 terry/wildfly 란 이미지로 저장하고자 할 때에는 docker commit -a "Terry Chang" wildfly terrychang/wildfly 로 명령을 실행한다. 이때 a 옵션은 이미지를 제작한 제작자를 설정하는 옵션이다.

 

23. 이미지를 특정 파일로 백업하고 특정 파일을 이미지로 복구할때 save/load 명령을 사용할 수 있다. 이와 비슷한 것으로 export/import 가 있다. 이 둘의 차이는 save/load는 그 대상이 이미지인것에 반해 export/import는 그 대상이 컨테이너(엄밀하게 말하면 export 명령을 이용해서 컨테이너를 특정 파일로 백업한뒤에 import를 이용해서 그 특정 파일을 이미지로 복구하는 것이다. 이 부분 헷갈리지 말도록)이다.

예를 들어 terrychang/wildfly란 이미지를 D:/dockerimages/customwildfly.tar 파일로 백업할때는 다음과 같이 한다.

 

docker save -o D:/dockerimages/customwildfly.tar terrychang/wildfly

 

이와는 반대로 D:/dockerimages/customwildfly.tar 파일을 terrychang/wildfly란 이미지로 복구할때는 다음과 같이 한다

 

docker load -i d:\dockerimages\customwildfly.tar

 

24. docker 이미지를 빌드할때는 빌드할때 필요한 파일들만 들어가 있는 디렉토리에 Dockerfile을 넣고 빌드해야 한다. 이게 나름 중요한데 Docker는 이미지를 빌드 할때 빌드명령이 실행된 디렉토리에 있는 모든 파일을 특정 공간에 업로드하고 사용한다. 이 과정을 몰라서 Dockerfile 파일을 가상 리눅스 머신 파일이 있는 곳에 넣고 빌드를 하다보니 빌드랄 할때 가상 리눅스 머신 파일이 같이 올라가면서 이 올라가는 과정만 계속 하다가 결국 뻗어버리는 증상이 있었다. 그래서 빌드를 할때는 Dockerfile 파일과 Dockerfile 안에서 빌드할때 사용되는 파일 및 디렉토리만 있게 하고 빌드 명령을 실행시켜야 한다.

 

25. centos나 ubuntu docker 이미지 파일의 경우 경량화(?)를 하는 이유로 백그라운드 서비스를 관리하는 systemd가 빠져있다. 그래서 centos의 경우는 centos에서 배포하는 centos/systemd 이미지를 사용하면 systemd를 이용한 백그라운드 서비스 관리를 이용할 수 있다. 이때 /usr/sbin/init 을 실행해야 한다. 이 init이 pid가 1이어야 systemd를 이용할 수 있다. 문제는 init을 실행하면서 별도 shell script(a.sh 라고 가정하자)를 실행해야 하는 상황이 있을수가 있는데 그럴때는 별도 shell script 파일(b.sh 라고 가정하자)을 만들고 이 파일에서 /usr/sbin/init과 a.sh가 실행이 되도록 하면 된다. 이때 주의할 점이 있는데 반드시 /usr/sbin/init 이 가장 마지막에 실행되도록 shell script를 만들어야 한다. 나는 pid가 1이어야 한다고 생각해서 /usr/sbin/init을 가장 먼저 실행되게끔 했는데 이렇게 하니까 b.sh가 실행이 되지 않는 문제가 발생하였다.  다음은 wildfly의 standalone.sh와 /usr/sbin/init이 실행되게끔 한 shell script 파일 소스이다.

 

#!/bin/bash

# background로 wildfly를 실행시킨다. 
# 원래는 /opt/jboss/wildfly/standalone/log/server.log 파일에 로그를 기록하지만 
# 별도 로그파일을 만들어서 보고 싶을 경우엔 
# /dev/null 부분을 특정 로그(ex: /usr/log/wildfly.log)로 지정하면 그 파일에 로그를 기록하게 된다

nohup /opt/jboss/wildfly/bin/standalone.sh -b 0.0.0.0 -bmanagement 0.0.0.0 > /dev/null &
exec /usr/sbin/init

 

26. 25번의 연장선상에 해당되는 설명인데 /usr/sbin/init을 실행할 경우 Dockerfile의 ENV를 이용해 설정한 환경변수가 이미지에 적용이 되지 않는다. 원래 ENV를 이용해서 환경변수를 설정하면 이것이 빌드할때 이미지에 반영되어 이 이미지를 컨테이너로 실행시킬때 해당 환경변수가 OS에 적용이 되는데 /usr/sbin/init이 실행되면 이 기능이 되지를 않는다(리눅스를 몰라서 구체적으로 설명하기는 어렵지만 구글링을 해보면 이 증상은 당연한 것이라고 나온다) 그래서 이와 같이 /usr/sbin/init이 실행되는 이미지가 컨테이너로 실행이 되는 상황에서 환경변수가 인식이 되게끔 할려면 /etc/environment 파일에 해당 환경변수가 기록이 되게끔 Dockerfile에 서술해주면 된다. 예를 들어 다음의 코드를 Dockerfile 안에 넣어주면..

 

RUN echo -e "LANG=ko_KR.utf8\nLC_ALL=\nJAVA_HOME=/opt/java" > /etc/environment

 

/etc/environment 파일이 다음과 같이 된다. 

 
LANG=ko_KR.utf8
LC_ALL=
JAVA_HOME=/opt/java

 

그러나 위의 내용은 Dockerfile을 통해 빌드를 해서 이미지를 만든뒤 컨테이너로 올려 실행할때의 발생되는 문제이지 Dockerfile에서 ENV로 설정한 환경변수를 빌드하는 과정에서 해당 환경변수를 이용할때는 문제가 없다. 예를 들어 ENV JAVA_HOME=/opt/java 로 했다고 가정할 경우 Dockerfile에서 이 환경변수를 이용하기 위해 RUN mv jdk* ${JAVA_HOME} 로 했을 경우 JAVA_HOME은 /opt/java로 인식되어 mv 명령을 수행하게 된다. 즉 빌드하는 과정에서 ENV로 설정한 환경변수를이용하는데는 문제가 없으니 오해없길 바란다.  

 

27. 25,26번의 연장선상에 해당되는 내용인데 26번에서 /usr/sbin/init을 실행하면 ENV로 설정한 환경변수가 이미지에 적용이 안되어 컨테이너에서 이 환경변수를 이용할 수 없다고 했다. 이런 문제때문에 환경변수를 이용해 지정하게 되는 PATH 또한 이 영향을 받게 된다. 이 부분을 해결할려면 /etc/profile.d 디렉토리에 별도의 shell script 파일이 생성되게끔 해서 컨테이너가 시작될때 이 shell script가 자동으로 실행이 되도록 한다. 예를 들어 Dockerfile에 ENV JAVA_HOME=/opt/java 을 넣고 다음의 코드를 Dockerfile에 넣어주면

 

RUN echo -e "export PATH=$PATH:$JAVA_HOME/bin" > /etc/profile.d/path.sh

 

빌드하는 시점에 $JAVA_HOME은 /opt/java로 인식이 되어 /etc/profile.d/path.sh 파일엔 export PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/opt/java/bin 가 들어가게 된다.

 

28. github에서 빌드 관련 파일들을 올려두고 Docker Hub와 연동해서 이미지 빌드 작업을 진행 할 경우 간혹 이런 메시지를 볼 수 있다

 

/assets/setup.sh: /bin/bash: bad interpreter: Text file busy

 

로칼에서 빌드할때는 이런 에러가 없었는데 Docker Hub로 연동해서 빌드할때 이런 에러 메시지가 나온다. 구글링을 한 바로는 이럴때 해당 shell 스크립트 파일을 실행하기 전에 sync 명령어를 실행하라고 해서 이 에러메시지를 잡았다. 

위의 에러 메시지가 나타나게 된 Dockerfile 코드를 예를 들어 구체적으로 설명하자면 에러 메시지가 나올당시의 Dockerfile 코드는 다음과 같이 되어 있었다

 

RUN chmod +x /assets/setup.sh && /assets/setup.sh

 

위의 코드를 다음과 같이 에러 메시지를 유발시키는 shell script(여기서는 /assets/setup.sh) 파일 앞에 sync를 추가로 넣어 실행이 되게끔 했다.

 

RUN chmod +x /assets/setup.sh && sync && /assets/setup.sh

 

이렇게 코드를 수정한뒤 빌드해서 에러 없이 빌드를 마쳤다

 

29. docker 컨테이너를 올리는 과정에서 다음의 에러 메시지를 docker logs 명령을 통해 확인되는 경우가 있다

 

standard_init_linux.go:195: exec user process caused “no such file or directory”

 

이 에러가 발생하는 상황이 딱히 정해져 있지는 않아서 지금 말하는 이 상황에서만 발생한다고 말할수는 없어보이나 나의 경우는 지금 말하는 이 상황에서만 발생하여서 이에 대해 정리를 하고자 한다.

 

docker 이미지를 만드는 과정에서 이런 저런 이유로 shell 파일들을 안에 넣게 되는데, 이때 shell 파일의 개행문자 처리 방식이 Windows일 경우 이 증상이 나타난다. 그래서 이럴땐 shell 파일의 개행문자 처리 방식을 Unix로 바꿔주면 된다.

 

Notepad++의 경우엔 shell 파일을 연뒤에 notepad++ 맨 밑에 우측을 보면 Windows (CR LF) 라고 나오는 부분이 있다. 이것은 개행문자를 Windows 형태 즉 \r\n의 초합으로 한다는것을 의미한다. 이 Windows (CR LF) 라고 나오는 부분을 마우스 우클릭하면 조그만 팝업 메뉴가 나타나는데 여기서 Unix 형식으로 변환 을 선택하면 개행문자 처리 방식을 Unix 형태 즉 \n 형태로 바꾸게 된다.

 

30. network와 관련하여 별도의 설정을 하지 않으면 docker container는 외부 인터넷 망에 대한 연결은 가능한 상태이지만 정작 docker를 운영하는 host와는 연결이 되지 않는 상태가 된다. 그래서 host와의 연결을 하기 위해 bridge 네트워크를 생성해야 할 필요성이 있다. 이럴때 다음과 같이 bridge network 를 생성한 뒤에 이를 이용하면 된다

 

docker network create -d bridge --subnet 192.168.100.0/24 --gateway 192.168.100.1 dockernet

 

docker network --help 하면 괸련 명령어의 설명을 볼 수 있는데 create는 생성할때 사용하는 명령이다 -d로 네트워크를 관리할 드라이버를 지정해야 하는데 여기엔 bridge와 overlay 이렇게 2개가 있다. 만약 container가 해당 container를 운영하는 host 및 그 host가 운영하는 container들 하고만 통신한다면 bridge 로 설정하면 된다. 그러나 이 container가 자신을 운영하는 host 뿐만 아니라 기타 다른 docker host 및 이 host가 운영하는 container들과 통신해야 한다면 이때는 overlay 로 설정한다(-d를 지정하지 않으면 default로 bridge로 설정된다) --subnet 과 --gateway는 이 생성된 network가 이용하게 될 ip 영역대와 gateway를 지정하게 된다. 위의 예시를 들어 설명한다면 192.168..100.2~255 번까지의 ip 주소들을 생성하고자 하는 network를 사용할 container가 자신의 ip 주소로 사용하게 되고, 이에 대한 gateway는 192.168.10.1을 사용하게 된다. 마지막으로 dockernet은 생성하게 되는 network의 이름을 지정하는 것이다. 이렇게 하면 docker network ls 명령을 이용해서 현재 생성된 network 들의 목록을 보게 될 때 dockernet 이란 이름으로 현재 생성한 network가 보이게 된다.

 

이렇게 network를 생성하게 되면 이제 이렇게 생성된 network를 docker container가 이용해서 사용하도록 해주어야 한다. 이것은 docker run을 이용해서 container를 실행시킬때 --net=dockernet 을 주어 docker container가 dockernet 네트워크를 이용하게끔 해주면 된다. 또 docker-compose 에서 이를 이용할땐 다음과 같은 방법으로 사용해주면 된다.

 

version: '2.1'

 

services:

  terrycentos:

    image: furywolf/centosprod

    container_name: centosprod

    cap_add:

      - SYS_ADMIN

    volumes:

      - /sys/fs/cgroup:/sys/fs/cgroup:ro

      - d:/docker/volumes/centos:/mnt/shared:rw

    ports:

      - "21:21"

      - "2122:22"

      - "64000-64010:64000-64010"

    networks:

      dockernet:

        ipv4_address: 192.168.100.200

networks:

  dockernet:

    external: true

 

services:terrycentos:와 같이 서비스명을 지정한 상태에서 하위에 networks란 멤버를 둔뒤에 여기에 사용하고자 하는 network인 dockernet을 지정한다. 그리고 networks:dockernet: 을 통해 해당 dockernet network를 지목한 상태에서 external: true를 줌으로써 docker host로의 접속이 허용이 되게끔 한다. docker run의 경우 host로의 접속이 허용되게 하는것은 기본 설정인데 만약 이를 막고자 할경우(host 접속하는 것을 막고자 하는 경우) --internal 옵션을 docker run 명령의 옵션으로 주면 된다

 

31. 30번의 경우는 container가 host로 접속을 하는 것에 해당하지만 host가 container가 가지고 있는 ip로의 접속이 된다는 뜻은 아니다. 그래서 이것을 하기 위해서는 이제부터 설명하게 될 설정을 해야 할 필요가 있다. 이 방법을 하면 30번에서 설명했던 container에서 host로의 연결도 같이 해결되기 때문에 30번에서 설명한 설정을 할 필요가 없다.

docker for windows 의 경우 container에 할당되는 ip는 192.168.100.X 형태의 내부 ip로 받게 되어 있다. 그러나 Windows 10에서 Network 관련 설정을 보면 docker와 관련된 ip 설정은 10.0.75.X 형태의 ip 설정만 존재하고 있다. 즉 192.168.100.X와 10.0.75.X와의 연결고리가 존재하지 않는 것이다. 그래서 이를 해결하기 위해 Windows 10 명령 프롬프트 창(CMD로 실행되는 명령 프롬프트창)을 관리자 권한으로 실행시킨 뒤 다음의 명령을 입력한다

 

route -p add 192.168.100.0 MASK 255.255.255.0 10.0.75.2

 

이렇게 입력하면 서로 다른 두 ip 영역대에 대한 연결관계가 성립이 되어 host와 container간의 ip로의 통신을 할 수 있게 된다(host에서는 container의 ip를 통한 통신이, container에서는 host의 ip를 통한 통신이 가능해진다)

 

위의 내용은 docker for windows에서 해당하는 내용이지 docker for mac이나 linux에서 운영중인 docker 에서도 사용가능한 방법인지는 확인되지는 않았다.

 

참고한 내용은 여기에서 했다(질문자인 whitecolor가 남긴 세번째 답변 글에서 참조했다)

 

중요 : 이걸 확인하려고 할때 ping을 이용해서 확인하면 안된다. docker conatainer 에서 host인 windows 로 통신여부를 체크할때는 ping을 써도 상관없지만 host인 windows에서 docker container로 통신여부를 체크할때는 ping을 사용하면 안된다. windows에서 port ping을 체크할 수 있는 프로그램인 tcping 을 다운받아서 특정 포트가 열려있는 컨테이너에서 포트에 대한 통신으로 체크하면 된다(ex : tcping 172.100.0.2 8080)

 

이것을 설정할 경우 개인적으로 가장 큰 장점이라 꼽히는 것은 container ip에 container port를 직접적으로 접근할 수 있다는 것이다. 예를 들어 우리가 container의 port를 이용하고자 할때 docker run 명령에서 -p 옵션을 사용하게 된다. -p 3080:8080 이렇게 주면 host의 3080 포트를 container의 8080 포트와 연결하는 것이다. 그래서 host ip:3080 하면 container의 8080 포트와 연결을 하게 되는 것이다. 그러나 container ip를 직접 접근할 수 있게 되면 container ip:8080 하는 식으로 host를 거치지 않고 container의 포트를 직접 접근할 수 있게 된다

 

※ 주의사항

 

처음 30번에서 설명한 route 설정을 할때 IP를 다음과 같이 설정했었다.

 

route -p add 172.0.0.0 MASK 255.0.0.0 10.0.75.2

 

이렇게 설정하니까 얼마 안있어 구글을 이용해서 검색을 하는데 문제가 생겼다. 계속 timeout이 걸렸는데..

이게 어떤때는 검색이 될 때도 있었고 되지 않는 때도 있어서 애를 좀 먹었다.

나중에 원인을 파악해보니 구글(www.google.co.kr) 도메인 주소가 DNS 서버를 통해 IP로 번역이 될때 172.217.160.67 이었는데..

route로 172로 시작하는 모든 IP 주소를 10.0.75.2로 연결하게끔하다보니..

구글 도메인 주소가 10.0.75.2를 통해 연결되는 상황이 벌어졌다.

이로 인해 검색이 되지 않는 문제가 생겼다.(검색이 되었을때의 IP 주소는 172로 시작하는 IP 주소가 아니었다)

그래서 docker container에 ip를 부여할때는 잘 알려지지 않고 사용하지 않는 IP 주소로 최소한의 범위로 route를 해주는게 좋다.

위에서와 같이 172.0.0.0 이런식으로 172로 시작하는 모든 주소 이런식으로 하지 말고..

 

route -p add 192.168.100.0 MASK 255.255.255.0 10.0.75.2

 

192.168.100 으로 시작하는 주소만 연결되게끔 하고 (MASK 설정도 IP 구성하는 4개 중 앞의 3개에 255를 설정하여 3개는 항상 고정이 되게끔 해준다)..

docker container가 사용할 네트워크도 위에서 설명한 것과 같이 192.168.100.0/24 이런 식으로 앞의 3개는 항상 고정되게끔 해주면 이런 상황을 피할수가 있다.

기존에 잘 되는 도메인 주소가 제대로 동작하지 않을때 해당 도메인 주소의 ip를 알아낸 뒤 이 ip가 docker 관련쪽으로 route 되는지 확인해보자

 

32. docker-compose.yml 파일에서 volume 항목을 다음과 같이 설정해야 할 때가 있다

 

/sys/fs/cgroup:/sys/fs/cgroup:ro

 

이것을 설정하는 이유는 systemctl 같은 서비스 기동/종료 명령을 사용하기 위해 설정하게 되는데 docker for windows 가 버전업이 되면서 docker-compose 명령 사용시 이 설정 부분에 에러가 발생하는 경우가 있다

이때는 windows 환경 변수의 시스템 변수에 COMPOSE_CONVERT_WINDOWS_PATHS 항목을 만들어 여기에 1을 설정해주면 해결된다

 

33. 최근에 docker 를 docker for windows 가 아닌 vagrant 기반으로 바꾸면서 이슈가 발생한 것이 있다. docker for windows 를 설치할 경우 docker 가 실행되는 곳은 localhost 이지만 vagrant 기반으로 할 경우 docker 는 vagrant의 linux vm에서 실행이 되기 때문에 docker 이미지를 빌드하는 경우 이를 vagrant의 linux vm에 1차적으로 들어가야 하기 때문이다. 즉 docker host가 localhost가 아닌 vagrant vm이 되어버렸기 때문에 docker 관련 명령을 실행할 경우 localhost가 아닌 vagrant vm 하에서 실행한 결과가 나와야 하기 때문이다. 이를 위해서 다음과 같은 절차를 거쳤다(이 설명대로 할 경우 vagrant 설치 및 vm 생성, 그리고 거기에서의 docker 설치는 이미 되어 있어야 하기 때문에 여기서는 그것과 관련된 내용은 말하지 않겠다)

 

- 나는 centos 기반의 vagrant vm에 docker를 설치했는데 docker를 설치한 후 systemctl 기반에서 제어되도록 하기 위해 다음의 과정을 미리 거쳐놨다

 

sudo systemctl enable docker

 

- 위의 과정을 거치면 /usr/lib/systemd/system 경로에 docker.service 파일이 생기게 되는데 이 경로로 이동한뒤 이를 vi 에디터로 연다. 이때 sudo 명령어를 이용해서 연다

 

sudo vi docker.service

 

- vi 에디터로 열면 [Service] 항목에 다음과 같은 내용이 있다(이 내용과 일치되지 않을수는 있겠으나 ExecStart 부분을 보면 된다)

 

ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

 

여기서 -H fd:// 부분을 다음과 같이 수정한다

 

ExecStart=/usr/bin/dockerd -H tcp://0.0.0.0:2375 --containerd=/run/containerd/containerd.sock

 

이렇게 하면 docker daemon을 2375번 포트를 통해 접속할 수 있게 해주기 때문에 외부에서도 docker daemon이 접속 가능하게 되어 docker client가 해당 서버의 docker daemon에 접속하여 명령을 내릴 수 있게 된다.

 

docker.service 파일을 저장한 뒤 다음의 명령들을 실행한다

 

sudo systemctl daemon-reload

sudo systemctl restart docker

 

이렇게 한 뒤 netstat -tnlp 를 실행하면 해당 vagrant vm에 열려있는 port 중 다음과 같이 위에서 설정한 2375 번 포트가 열려있는 것을 발견할 수 있다

 

tcp6       0      0 :::2375                 :::*                    LISTEN      -

 

(IntelliJ에서 docker 접속시 다음과 같은 설정을 통해 접속이 되는 것을 확인했다)

 

 

위의 그림에서 192.168.100.2 는 vagrant에서 실행되고 있는 docker가 설치된 centos vm의 ip 이다. 이렇게 설정하면 192.168.100.2 에서 실행중인 docker daemon에 접속하여 접속된 daemon에서 docker 관련 명령을 실행할 수 있다.

 

34. 33번의 과정을 거치고 난 뒤에 발생한 또 하나의 문제는 vagrant의 linux vm에서 docker 명령이 실행되지 않는 문제가 발생되었다. 이를 해결하기 위해 다음의 명령어로 환경변수를 설정했다

 

export DOCKER_HOST=tcp://0.0.0.0:2375

 

35. 31번 글과 비슷한 내용의 글인데 vagrant 를 알게 된 뒤로는 docker 환경을 vagrant centos 에 docker를 설치한 뒤 이를 활용하는 식으로 접근하고 있다. 31번과 같이 windows에서 vagrant 안에서 실행중인 docker container와 직접적인 통신을 하기 위해 route 문을 사용했지만 route 명령에 대한 문외한으로 인해 늘 실패했었는데 이제 성공하게 되어서 글을 남긴다.

docker container가 가지고 있는 ip가 10.10.20.15이고 vagrant 가상머신(docker가 설치되어 있는 가상머신)의 ip가 192.168.100.2 라고 가정할 경우 route add 명령을 다음과 같이 작성해주면 된다

 

route -p add 10.10.20.0 MASK 255.255.255.0 192.168.100.2

 

이렇게 하면 192.168.100.2의 IP를 가지고 실행중인 docker가 설치된 vagrant 가상머신에서 10.10.20.X 대의 IP를 가지고 실행중인 docker container와 windows 간의 통신이 가능할 수 있게 된다. 

 

36. ENTRYPOINT(docker-compose 에서는 entrypoint)와 CMD(docker-compose 에서는 command) 에 대한 정리

ENTRYPOINT와 CMD 모두 docker image에서 명령어를 실행한다는 점에서는 차이가 없다. 다만 이를 실행하는 방식에 있어서 차이가 존재한다.

일단 이미지 안에서 동작하는 부분에 대하여 한번 정리를 하자면..

ENTRYPOINT와 CMD 모두 다음과 같은 2가지의 형태로 실행시킬수 있다.

 

  1. ENTRYPOINT(또는 CMD) "mycommand", "myparam1", "myparam2"
  2. ENTRYPOINT(또는 CMD) ["mycommand", "myparam1", "myparam2"]

1번의 경우는 다음과 같은 형태로 실행이 된다

 

/bin/sh -c 'mycommand myparam1 myparam2'

 

/bin/sh 에 설정되어 있는 shell을 이용해서 실행시키는 방식이다. 그러나 2번의 경우는 다음과 같이 실행된다

 

mycommand myparam1 myparam2

 

이 경우는 직접 명령어를 실행시키는 방식이다. 이 두 방식의 실행 차이는 솔직히 잘 모르겠다. 왜냐면 결과는 같게 나오는데 왜 이렇게 따로 분리하는지는 모르겠다. 분명 차이가 존재할텐데..아무튼 이러한 형태로 실행된다.

 

지금까지의 설명은 Dockerfile 에서 ENTRYPOINT나 CMD 둘 중 하나만 사용했을때이다. 그러나 이 둘을 섞을 경우는 어떻게 실행이 되는가? ENTRYPOINT 든 CMD 든 설정 방법을 위의 1번이나 2번 모두 사용가능하기 땜에 조합이 가능한 경우의 수가 존재하게 된다. 자세한 내용은 여기를 참조하면 되며 링크된 내용을 기반으로 정리를 하자면 다음과 같다

 

  1. ENTRYPOINT "mycommand", "myparam1", "myparam2" 형태로 ENTRYPOINT를 사용하게 되면 그 다음에 CMD를 어떤 형태로 사용하든 최종 실행 명령 형태는 /bin/sh -c mycommand myparam1 myparam2 형태로 실행된다(한마디로 CMD 설정은 무시가 된다)
  2. ENTRYPOINT ["mycommand", "myparam1", "myparam2"] 형태로 ENTRYPOINT를 사용하게 되면 CMD 형태를 어떤 형태로 사용하든 CMD에 사용한 값들은 ENTRYPOINT에서 사용된 명령어의 파라미터로 사용하게 된다(CMD에서 명령어를 사용해도 이 명령어 문자열 값 자체가 ENTRYPOINT에서 사용된 명령어의 파라미터로 사용되어진다)

이러한 실행방법은 나름 의미가 있다. Docker image를 만들때 ENTRYPOINT를 사용하지 않고 CMD만 사용할 경우 docker run 실행시 CMD에서 설정한 명령어가 아닌 다른 명령어를 실행시킬수 있기 때문에 보안에 문제가 있다. 그러나 ENTRYPOINT를 사용하게 되면 CMD에서 설정한 명령어는 명령어가 아닌 ENTRYPOINT 명령어의 파라미터로 실행되기 때문에 docker run 실행시 ENTRYPOINT 명령어가 아닌 다른 명령어를 실행시킬수 없다(이것도 보면 --entrypoint 옵션을 사용해서 바꿀수 있기 때문에 이 방법도 보안에 완벽한 것은 아니다. 그렇기때문에 이미지를 만들때 실행시 문제의 소지가 있는 명령어나 shell script는 아예 빼는 것이 낫다)

 

37. 36번의 내용을 급히 정리하게 된 계기가 있었는데 mongodb를 공부하기 위해 docker-compose 파일을 다음과 같이 만들었지만 정상적으로 동작되지 않았다. 다음은 docker-compose 파일의 일부분이다.

 

version: '2.1'

services:
  mongo:
    image: mongo:4.2.5-bionic
    container_name: mongo
    restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: mongodb
    ports:
      - "27017:27017"
    ...
    entrypoint: ["mongod","--config","/etc/mongod.conf"]
    ...

 

docker-compose 파일 내용에 대해 설명을 하자면 관리자 계정을 만들기 위해 MONGO_INITDB_ROOT_USERNAME 과 MONGO_INITDB_ROOT_PASSWORD 에 관리자 계정과 비밀번호를 설정하고 mongodb 의 환경설정 파일인 /etc/mongod.conf 파일을 사용하기 위해 entrypoint로 mongod 명령어에 파라미터로 --config /etc/mongod.conf 를 주었다. 그런데 이렇게 실행하니 만들어져야 할 관리자 계정과 비밀번호가 만들어지지 않아서(이 부분에 대해서는 mongodb 가 실행되어 올라올때 나오는 log를 보면 알 수 있다. 관리자 계정을 만들 경우 관리자 계정을 만드는 것과 관련된 내용이 log에 나온다) 관리자 계정으로 접속을 할 수 없었다. 그 원인은 내가 사용했던 entrypoint 에 문제가 있었다.

 

지금부터 mongodb docker 이미지를 배포하는 입장에서 생각해보자. mongodb docker 이미지를 만들때 환경변수에 관리자 계정과 비밀번호를 설정하게 하고 mongod 명령어 실행시 자동으로 관련 작업을 같이 하게끔 한다고 가정해보자. 일단 mongod 명령어 실행시 계정을 만드는 option은 존재하지않는다. 때문에 계정을 만드는 작업을 진행한다면 mongod 명령어를 실행한뒤 mongo 명령어를 실행시켜 관련 작업을 진행하게 된다. 여기를 클릭해보면 mongodb docker 이미지를 만드는데 사용되는 Dockerfile을 볼 수 있는데 여기서 사용된 ENTRYPOINT로 Dockerfile 과 같은 디렉토리에 있는 docker-entrypoint.sh 파일을 실행시키고 CMD로 mongod 를 사용하고 있다. docker-entrypoint.sh를 먼저 실행시켜서 위에서 언급한 작업들을 진행하는 것이다. 그러나 docker-compose에서 entrypoint 로 mongod 를 실행시키게금 설정하면 Dockerfile에 언급한 docker-enttrypoint.sh 파일이 실행되는 것이 아니라 바로 mongodb 를 실행시켜버리기 때문에 계정 생성 같은 관련 부가 작업이 일절 실행되지 않게 되는 것이다. 그래서 이 부분을 아래과 같이 고쳐서 docker-entrypoint.sh 명령어의 실행은 유지시키면서 부가적으로 /etc/mongod.conf 파일을 환경파일로 해서 mongodb 가 실행되게끔 수정했다

 

version: '2.1'

services:
  mongo:
    image: mongo:4.2.5-bionic
    container_name: mongo
    restart: always
    environment:
      MONGO_INITDB_ROOT_USERNAME: admin
      MONGO_INITDB_ROOT_PASSWORD: mongodb
    ports:
      - "27017:27017"
    ...
    command: ["-f","/etc/mongod.conf"]
    ...

 

entrypoint 대신 command 로 바꿔서 기존 Dockerfile의 ENTRYPOINT는 실행이 되게끔 했고 기존 Dockerfile에서 CMD로 mongod 를 실행하고 있기 때문에 기존 Dockerfile 에서 실행되는 mongod 에 추가 옵션이 붙어서 실행이 되는 형태로 command 항목에 ["-f", "/etc/mongod.conf"] 로 주어서 최종적으로는 Dockerfile ENTRYPOINT 실행후 mongod -f /etc/mongod.conf 가 실행이 되는 형태로 만들었다. 이렇게 docker-compose 파일에서 entrypoint와 command를 사용할때는 해당 이미지의 Dokerfile에서 사용되는 ENTRYPOINT와 CMD를 보면서 본인이 의도하는 목적대로 실행되게끔 설정하는 것이 좋다 

 

38. 현재 실행중인 docker container 내부에 들어와 있는 상태(attach)에서 exit 명령어를 입력해서 container 밖으로 나오게 되면 container가 중지된다. container를 중지시키지 않고 빠져나올려면 ctrl + p , ctrl + q 를 눌르면(ctrl키를 눌르고 있는 상태에서 p키를 눌르고 q키를 눌른다) container를 중지시키지 않으면서 container 밖을 빠져나와 host로 돌아갈 수 있다. 그러나 내 환경인 vagrant 기반의 centos docker 환경에서는 이 명령어 조합이 먹어들어가질 않았다. terminal program으로 mobaxterm을 사용하고 있어서 혹시 이것 때문인가 싶어 windows powershell을 이용해서도 해봤지만 되질 않았다. ctrl+p, ctrl+q, ctrl+c 까지 해줘야 빠져나올수 있었다.

 

39. 시작하세요! 도커/쿠버네티스 책을 보면서 /etc/docker/daemon.json 파일을 알게 되었다. 이 파일이 하는 역할은 docker service를 시작할때 관련 옵션들을 이 파일을 통해서 할 수 있다. 원래 DOCKER_OPTS 라는 환경변수에 관련 옵션들을 설정할 수 있지만 설정해야 할 옵션이 많아서 체계적으로 정리가 되어 보여야 할 필요성이 있을때는 json 포맷 형태를 가지고 있는 daemon.json 파일에다가 관련 옵션을 설정할 수 있다(나는 기존에 vagrant 에서 docker 설정시 docker를 install 하는 shell script에 /etc/docker/daemon.json 파일을 생성하는 내용을 넣어놓고 있었다. 그러나 그 당시에는 이 파일의  의미를 다르게 알고 있었는데 이번에 공부를 하면서 이 파일의 용도를 알게 되었다. 참고로 내가 사용하는 daemon.json 파일의 내용은 다음과 같다

 

{
  "exec-opts": ["native.cgroupdriver=systemd"],
  "log-driver": "json-file",
  "log-opts": {
    "max-size": "100m"
  },
  "hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2375"],
  "storage-driver": "overlay2",
  "storage-opts": [
    "overlay2.override_kernel_check=true"
  ]
}

 

40. 39번 글을 쓰게 된 상황이 이 40번 글을 쓰게끔 되었는데 나는 vagrant에서 기동되는 centos에 docker를 설치하고 사용했다. 그러다보니 docker를 접속해야 하는 클라이언트가 2종류가 있었는데, 하나는 docker가 기동중인 centos에서 docker를 접속해야 하는 경우(이것에 대해서 부가적으로 설명하자면 dockerd 라는 프로그램을 실행하여 docker daemon이 올라가고 우리가 흔히 아는 docker 명령어가 이 docker daemon과 통신하여 작업결과를 보여주게 된다. 즉 우리가 사용하는 docker 명령어는 이 docker daemon과 통신하는 클라이언트인 셈이다)와 외부 프로그램(docker plugin이 설치된 eclipse나 intellij)에서 방금 설명한 docker daemon과 통신하여 작업하세 되는 Remote 호출이 바로 그것이다. centos에서 docker 명령어를 사용하는 것과 Remote 호출을 동시에 하는 법을 알지를 못해서 centos에서 docker 명령어를 사용하는 것도 Remote 호출하는 형태로 구현해서 사용하고 있었다. 이렇게 하는 방법은 다음과 같다

 

  • dockerd 명령어를 사용할때 -H 옵션을 주어서 해당 host ip와 관련 포트를 설정한다(dockerd -H tcp://0.0.0.0:2375)
  • docker 명령어를 사용할때 -h 옵션을 주어서 docker 가 접속해야 할 host ip와 포트를 지정한다. 그러나 매번 이렇게 붙여가며 docker를 실행하는 것은 불편하기 때문에 DOCKER_HOST 라는 환경변수를 만들어 거기에 host ip와 관련 포트를 설정한다(echo expose DOCKER_HOST=tcp://0.0.0.0:2375 >> etc/profile) 환경변수로 등록해놓으면 docker 명령어 실행시 -h 옵션을 붙여서 어디로 접속해야할지 명시하지 않아도 된다

그러나 최근에 39번 글에서 언급했던 책을 보면서 굳이 local에서 접속하는 것도 Remote 호출하듯이 설정하지 않아도 되는 방법을 알게 되었다. Local에서 접속하는것은 unix socket으로 접속하게끔 하고 Remote 에서 접속하는 것은 기존과 같이 tcp 소켓통신으로 하는 식이다. 39번의 daemon.json 파일의 내용에서 hosts key에서 사용한것이 바로 그 방법이다. hosts key에 값을 설정할때 배열 개념을 사용할 수 있기 때문에 이렇게 2개를 지정해주면 된다. 당연 DOCKER_HOST 환경변수 등록작업은 할 필요가 없다.

 

41. centos에서 docker를 설치한뒤 systemctl을 이용해서 docker를 service로 등록하게 되면(sudo systemctl enable docker.service) docker service 파일이 /usr/lib/systemd/system 디렉토리에 docker.service 란 파일로 생성된다. 이 파일에는 docker를 service로 운영하기 위해 실행되는 설정들이 있는데 이 중 다음과 같은 line이 있다(완전히 똑같지는 않을수 있으니 참고로 보길 바란다)

 

ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

 

ExecStart는 서비스를 시작할때 어떤 프로그램을 실행시키는지를 지정하는 항목이다. 여기서 보면 40번 글에서 언급했던 dockerd를 실행시키고 있다. 이때 보면 40번 글에서 설명했던 -H 옵션을 사용하고 있는 것을 볼 수 있다. -H 옵션을 사용해서 fd:// 를 설정하고 있는데 39번 글에서 설명했던 daemon.json 파일에서 dockerd가 접속해야 할 host를 설정해서 사용하고 있기 때문에 중복되고 있는 것이다. 즉 dockerd 명령어에서 -H 옵션을 사용해서 접속해야 할 host를 지정하고 있고 이와 동시에 daemon.json 파일에서 hosts key 값의 value 로도 지정하고 있다. 이렇게 같은 기능을 dockerd 명령어 옵션과 daemon.json 파일에서 동시에 설정하고 있는 것이다. 이럴 경우 우선순위는 dockerd 명령어의 -H 옵션으로 준것이 우선순위가 올라간다. 그렇기 땜에 dockerd 명령어의 -H 옵션을 주어 접속해야 할 host를 지정하게 되면 daemon.json 파일에서 hosts key 값에 설정된 값들은 무시가 된다. 그래서 관리를 할려면 같은 옵션이 양쪽에서 사용되지 않게끔 조정해줄 필요가 있다. 그래서 나는 ExecStart 항목의 dockerd -H fd:// 문자열을 dockerd로 바꾸는 작업을 통해서 이러한 문제를 해결했다. 다음이 그렇게 한 방법이다.

 

sudo sed -i 's|dockerd -H fd://|dockerd|g' /usr/lib/systemd/system/docker.service

 

dockerd -H fd:// 란 문자열을 docker.service 파일에서 찾아 이를 dockerd 문자열로 바꿔주는 것이다. 그러면 -H fd:// 문자열이 없어지기 때문에 -H 옵션이 아닌 다른 옵션은 건드리지 않고 -H 옵션만 삭제하는 효과를 가져올 수 있다.

 

42. docker를 설정하게 되면 /var/run/docker.sock 파일의 권한을 바꿔야 한다. 이걸 하지 않으면 docker.sock 파일을 이용할 권한이 없다는 에러 메시지가 나오기 때문이다. 이때문에 다음과 같은 작업을 진행한다.

 

sudo chmod 666 /var/run/docker.sock

 

문제는 이 작업을 진행해도 docker service를 restart 하면 다시 이 docker.sock 파일이 재생성되면서 우리가 방금 위에서 언급했던 docker.sock 파일의 권한 설정작업을 다시 해야 하는 상황이 발생한다. 그래서 이를 방지 하기 위해 docker service가 시작된뒤에 이 권한 바꾸는 작업을 실행하게끔 41번 글에서 언급한 docker.service 파일에 설정해줘야 한다. docker.service 파일의 내용을 살펴보면 [Service] 라는 섹션에 41번에서 언급했던 ExecStart 항목이 있다. 여기에 ExecStartPost 란 항목을 새로 추가해서 거기에 이 권한 바꾸는 작업을 실행하게끔 설정해둔다. 그래서 전체적으로는 아래와 같이 [Service] 섹션을 다음과 같이 해주면 된다(이것도 이 글을 보는 사람이 실제 작업할때는 아래와 같이 완전히 같은 내용은 아닐수도 있다. ExecStartPost 항목이 어떤 내용으로 추가되었는지 참고하는 관점에서 아래의 [Service] 섹션 설정 내용을 보길 바란다)

 

[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd --containerd=/run/containerd/containerd.sock
ExecStartPost=/usr/bin/chmod 666 /var/run/docker.sock
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutSec=0
RestartSec=2
Restart=always

 

이미 docker가 설치가 된 뒤에 docker.service 파일을 수정하는 것은 어렵지는 않다. 그러나 vagrant의 경우는 vagrant 가상이미지 박스를 만드는 시점에서 이걸 해놔야 vagrant 가상머신을 날려버리고 새로 제작할때 번거로이 이런 설정작업을 안하게끔 할 수 있다. 그래서 docker를 설치하는 script 파일에서 다음의 내용을 넣었다.

 

sudo sed -i 's|ExecReload=/bin/kill|ExecStartPost=/usr/bin/chmod 666 /var/run/docker.sock\nExecReload=/bin/kill|g' /usr/lib/systemd/system/docker.service

 

위의 [Service] 섹션을 보면 ExecStartPost 항목은 ExecReload 항목의 윗줄에 들어가 있다. 특정 문자열을 새로이 지정하는 문자열로 바꾸는 개념에서 볼때 ExecReload=/bin/kill 문자열을 ExecStartPost=/usr/bin/chmod 666 /var/run/docker.sock\nExecReload=/bin/kill 문자열로 바꿔주는 방법으로 사용했다. ExecReload 항목앞에 \n 개행문자를 넣어서 ExecStartPost 항목 문자열을 기록한뒤 개행해서 ExecReload 항목이 기록되게끔 한 것이다.