포스트

Docker 기초 (4) - Working with Containers: run, logs, and exec

run의 다섯 옵션 -d, -p, -e, -v, -it부터 logs, exec, container 수명 원리까지 nginx와 MySQL 실습으로 정리합니다.

Docker 기초 (4) - Working with Containers: run, logs, and exec

Docker 기초 시리즈의 4편입니다. 전체 목차는 0편에 있습니다.

Docker는 hello.py처럼 실행을 마치면 종료되는 프로그램뿐만 아니라, nginx, uvicorn처럼 계속 실행되는 서버 프로그램도 구동할 수 있습니다. 앞서 살펴본 hello.py 실습은 Docker의 기본 동작을 이해하기 위한 실습이었습니다. 실제로는 server program을 container에서 지속적으로 실행하는 방식이 널리 사용됩니다.

먼저 nginx와 uvicorn의 역할을 간단히 살펴보겠습니다. 둘 다 client의 요청을 처리하는 server program이지만, 담당하는 역할은 다릅니다.

  • nginx (웹 서버): 요청이 오면 미리 만들어 둔 파일을 그대로 돌려줍니다. HTML과 이미지 같은 정적 파일을 나눠주는 역할이고, 뒤에 있는 다른 서버로 요청을 넘겨주는 입구 역할도 합니다.
  • uvicorn (애플리케이션 서버): 요청이 오면 Python 코드를 실행해서 응답을 만들어 돌려줍니다. FastAPI로 짠 앱을 실제로 실행해 주는 것이 uvicorn입니다.

Docker container는 내부의 process가 실행되는 동안 유지되므로, nginx나 uvicorn이 종료되지 않는 한 container도 계속 실행 상태로 남습니다. 이번 편에서는 이처럼 server process를 container에서 사용하는 방법을 알아보겠습니다.

docker run

docker run은 이미지를 실행하는 기본적인 명령어이며, 아래는 가장 기본 형태입니다.

1
docker run --name example nginx
  • docker: Docker CLI를 실행합니다
  • run: image로 container를 만들어 시작하는 하위 명령입니다
  • --name example: 만들 container의 이름을 example로 지정하는 option입니다. 생략하면 무작위 이름이 붙습니다
  • nginx: 실행할 image의 이름입니다. local에 없으면 Docker Hub에서 자동으로 pull 합니다

실행하면 nginx가 시작되면서 로그가 흐릅니다.

1
2
3
/docker-entrypoint.sh: Configuration complete; ready for start up
2024/07/25 09:15:00 [notice] 1#1: start worker processes
...

Log가 몇 줄 흐른 뒤 프롬프트가 돌아오지 않습니다. 멈춘 것이 아니라, server는 끝나지 않는 process라 nginx가 terminal을 점유한 채 계속 돌고 있는 것입니다. Ctrl + C로 빠져나오면 nginx가 종료되면서 container도 함께 멈춥니다. 확인했으면 지워둡니다.

1
docker rm example


docker run option

docker run의 option은 많지만, 자주 쓰는 것은 아래 다섯 계열입니다. 이번 편에서 이 option들로 실습을 진행하고, 전체 목록은 docker run --help로 확인할 수 있습니다.

옵션역할실습
-d백그라운드 실행 (detached)nginx
-p 호스트:컨테이너port를 호스트로 연결nginx
-e환경 변수 주입MySQL
-vvolume 연결MySQL
-it터미널 연결python

표의 volume은 container 바깥의 저장 공간입니다. container 안에서 쓴 파일은 container를 삭제하면 같이 사라지므로, 남겨야 할 데이터는 -v 이름:안의경로 형식으로 volume에 연결해 둡니다. 이번 편의 MySQL 실습에서 그 효과를 확인하고, volume의 종류와 동작 원리는 7편에서 다룹니다.


위 Option 들을 사용해서 Server를 띄우는 명령어를 사용해보겠습니다.

1
2
docker   run   -d   --name web   -p 8080:80   nginx
  ①       ②    ③        ④            ⑤          ⑥
  • docker: Docker CLI를 실행합니다
  • run: image로 container를 만들어 시작합니다
  • -d: 백그라운드로 실행합니다 (detached). 프롬프트가 바로 돌아옵니다
  • --name web: container 이름을 web으로 지정합니다. 이후 모든 명령에서 이 이름을 씁니다
  • -p 8080:80: 내 컴퓨터의 8080 포트를 container의 80 포트에 연결합니다
  • nginx: 실행할 image입니다. 로컬에 없으면 자동으로 pull 합니다

실행하면 64자리 문자열 하나만 출력되고 프롬프트가 돌아옵니다.

1
9f8e7d6c5b4a3210...   # 방금 만들어진 container의 ID

떠 있는지 확인합니다.

1
docker ps
1
2
CONTAINER ID   IMAGE   STATUS         PORTS                  NAMES
9f8e7d6c5b4a   nginx   Up 5 seconds   0.0.0.0:8080->80/tcp   web

STATUS가 Up이고, PORTS에 8080->80 연결이 보입니다. 브라우저에서 http://localhost:8080 을 열거나 curl로 확인합니다.

1
curl localhost:8080
1
2
3
4
5
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

설치 과정 없이 웹 서버가 하나 떴습니다.

-p의 방향에 주의

-p 8080:80이 필요한 이유는 VM 때문이 아니라 container의 network가 Host와 분리되어 있기 때문입니다. namespace가 container마다 IP와 포트 공간을 따로 주므로, 80은 container 자신의 포트이고 container 두 개를 띄우면 둘 다 각자의 80에서 들을 수 있습니다.


docker logs

1
2
docker   logs   -f   web
  ①       ②     ③    ④
  • docker: Docker CLI를 실행합니다
  • logs: container 메인 프로세스의 출력을 보여주는 하위 명령입니다
  • -f: 실시간으로 따라갑니다 (follow). 생략하면 지금까지의 로그만 출력하고 끝납니다
  • web: 대상 container 이름입니다

docker logs -f web을 켜 둔 채 브라우저에서 페이지를 두어 번 새로고침해 봅니다.

1
2
172.17.0.1 - - [25/Jul/2024:09:20:11 +0000] "GET / HTTP/1.1" 200 615 ...
172.17.0.1 - - [25/Jul/2024:09:20:13 +0000] "GET / HTTP/1.1" 200 615 ...

새로고침할 때마다 접속 로그가 한 줄씩 찍힙니다. 확인했으면 Ctrl + C로 빠져나옵니다. 이번의 Ctrl + C는 container를 멈추지 않습니다. 아까와 달리 지금 터미널이 잡고 있는 것은 nginx가 아니라 로그를 구경하는 logs 명령이기 때문입니다. docker ps로 web이 여전히 Up인 것을 확인해 보세요.

logs가 보여주는 것은 메인 프로세스의 표준 출력(stdout)입니다. container 앱이 로그를 파일이 아니라 stdout으로 내보내는 관행이 여기서 나옵니다. 그래야 docker logs와 로그 수집기가 잡을 수 있습니다.


docker exec

exec는 실행 중인 container 안에서 프로세스를 하나 더 띄우는 명령입니다. 쉘(bash)을 띄우면 container 안으로 들어가는 셈이 됩니다.

1
2
docker   exec   -it   web   bash
  ①       ②     ③     ④     ⑤
  • docker: Docker CLI를 실행합니다
  • exec: 실행 중인 container 안에서 명령을 실행합니다
  • -it: 내 터미널을 그 명령에 연결합니다. 쉘처럼 입력을 주고받는 명령에 필요합니다
  • web: 대상 container 이름입니다
  • bash: 실행할 명령, 여기서는 쉘입니다

실행하면 프롬프트가 바뀝니다.

1
root@9f8e7d6c5b4a:/#

지금부터 치는 명령은 전부 container 안에서 실행됩니다. nginx가 서비스 중인 페이지 파일을 찾아 바꿔봅니다.

1
2
3
ls /usr/share/nginx/html          # index.html이 보인다
echo "hello from container" > /usr/share/nginx/html/index.html
exit                              # container에서 나온다

나온 뒤 브라우저를 새로고침하거나 curl을 다시 쳐봅니다.

1
2
curl localhost:8080
# hello from container

기존의 페이지가 방금 쓴 문장으로 바뀌었습니다. 떠 있는 서버 안에 들어가 실제로 손을 댄 것입니다. 한 가지 짚어둘 점은, 바뀐 것은 이 container뿐이고 nginx image는 그대로라는 것입니다. 이 container를 지우고 새로 run 하면 다시 원래 환영 페이지가 나옵니다. image는 읽기 전용이고 container는 그 위의 쓰기 가능한 층이라는 2편의 구조가 여기서 확인됩니다.


docker inspect

container를 바꾸지 않고 들여다보기만 하는 명령들을 이어서 봅니다. web이 떠 있는 지금 하나씩 실행해 봅니다.

1
2
docker   inspect   web
  ①        ②       ③
  • docker: Docker CLI를 실행합니다
  • inspect: container의 설정과 상태 전체를 보여주는 하위 명령입니다
  • web: 대상 container 이름입니다

설정과 상태 전체가 긴 JSON으로 나옵니다. run에 어떤 옵션을 줬는지 기억나지 않을 때, 포트와 volume이 정말 연결되어 있는지 확인할 때 씁니다. 전부 읽을 필요는 없고, 필요한 항목을 grep으로 찾는 식으로 씁니다.

1
docker inspect web | grep -A 3 '"Ports"'   # 포트 연결 확인


docker stats

1
2
docker   stats
  ①       ②
  • docker: Docker CLI를 실행합니다
  • stats: container별 자원 사용량을 실시간으로 보여주는 하위 명령입니다. 이름을 붙이면(docker stats web) 그 container만 봅니다
1
2
CONTAINER ID   NAME   CPU %   MEM USAGE / LIMIT     MEM %
9f8e7d6c5b4a   web    0.00%   3.6MiB / 7.7GiB       0.05%

container별 CPU와 메모리 사용량이 실시간으로 갱신됩니다. 여러 container 중 어느 것이 자원을 차지하는지 찾을 때 씁니다. Ctrl + C로 나오고, logs 때와 마찬가지로 터미널이 잡고 있던 것은 구경하는 명령뿐이라 container에는 영향이 없습니다.


docker cp

1
2
docker   cp   web:/usr/share/nginx/html/index.html   .
  ①      ②                    ③                      ④
  • docker: Docker CLI를 실행합니다
  • cp: container와 호스트 사이에서 파일을 복사하는 하위 명령입니다
  • ③ 원본: container이름:안의 경로 형식입니다
  • .: 대상, 여기서는 호스트의 현재 폴더입니다
1
2
3
docker cp web:/usr/share/nginx/html/index.html .
cat index.html
# hello from container

방금 exec로 바꾼 파일이 내 폴더로 꺼내졌습니다. container 안에서 만들어진 결과 파일을 호스트로 가져오는 용도가 대표적이고, 원본과 대상을 바꿔 적으면 반대 방향(호스트에서 container 안으로)도 됩니다.

1
docker cp ./index.html web:/usr/share/nginx/html/   # 호스트에서 container로

nginx 실습은 여기까지입니다. 지우고 다음 실습으로 넘어갑니다.

1
2
3
docker stop web     # 실행 중인 container는 먼저 멈추고
docker rm web       # 지운다
rm index.html       # cp로 꺼낸 파일도 정리


MySQL 실습

나머지 두 옵션은 DB로 실습하는 것이 가장 잘 보입니다. MySQL 서버를 띄우는 한 줄입니다.

1
2
3
4
5
docker run -d \
  --name db \
  -e MYSQL_ROOT_PASSWORD=dev \
  -v mysql-data:/var/lib/mysql \
  mysql:8.4
  • -e MYSQL_ROOT_PASSWORD=dev: 환경 변수를 주입합니다. mysql image는 이 변수로 관리자(root) 비밀번호를 정하도록 만들어져 있고, 이 변수 없이는 시작하지 않습니다. 비밀번호 같은 설정을 image에 굽지 않고 실행 시점에 넣는 것입니다.
  • -v mysql-data:/var/lib/mysql: mysql-data라는 volume을 container의 데이터 폴더에 연결합니다. 앞이 volume 이름, 뒤가 container 안의 경로입니다. 이제 DB 데이터는 container가 아니라 volume에 쌓입니다.

첫 실행은 DB 초기화 때문에 오래 걸릴 수도 있습니다. 이 편에서 배운 logs로 준비됐는지 확인합니다.

1
docker logs db 2>&1 | grep "ready for connections"   # 이 줄이 나오면 준비된 것

MySQL은 로그를 stderr로 내보내므로 2>&1로 두 통로를 합쳐야 grep에 걸립니다. 아무것도 안 나오면 아직 초기화 중이니 잠시 후 다시 실행합니다.


이제 docker exec로 테이블을 만들고 데이터를 넣어 봅니다. 아래 명령어가 길어 보이지만, nginx 실습에서 배운 exec 뒤에 mysql client 호출이 붙었을 뿐입니다.

1
2
docker   exec   db   mysql   -uroot   -pdev   -e "SQL"
  ①       ②    ③     ④        ⑤       ⑥        ⑦
  • docker: Docker CLI를 실행합니다
  • exec: 실행 중인 container 안에서 명령을 실행합니다
  • db: 대상 container 이름입니다
  • mysql: container 안에서 실행할 명령입니다. MySQL이 기본 제공하는 명령줄 client입니다
  • -uroot: root 계정으로 접속합니다 (user). 옵션과 값을 붙여 적습니다
  • -pdev: 비밀번호는 run 할 때 -e로 넣은 dev입니다 (password). -p는 값을 띄어 쓰면 안 되는 옵션입니다. -p dev로 띄면 dev를 database 이름으로 해석해 버리고, -p만 쓰면 실행 후에 비밀번호를 물어봅니다. exec는 한 번 실행되고 끝나는 명령이라 물어볼 틈이 없어 붙여 쓴 것입니다
  • -e "SQL": 따옴표 안의 SQL을 실행하고 종료합니다 (execute). run의 -e(환경 변수)와는 무관한, mysql client 자신의 옵션입니다


실행할 SQL은 세 줄로, demo라는 database와 그 안의 memo 테이블(100자 문자열 body 열 하나)을 만들고, 두 행을 INSERT 하고, SELECT로 확인합니다.

1
2
3
docker exec db mysql -uroot -pdev -e "CREATE DATABASE demo; CREATE TABLE demo.memo (body VARCHAR(100));"
docker exec db mysql -uroot -pdev -e "INSERT INTO demo.memo VALUES ('first'), ('second');"
docker exec db mysql -uroot -pdev -e "SELECT * FROM demo.memo;"
1
2
3
body
first
second


비밀번호를 명령줄에 적었다는 Warning이 한 줄 나오는데 실습에서는 무시해도 됩니다. 이번에는 DELETE로 한 행을 지워봅니다.

1
2
docker exec db mysql -uroot -pdev -e "DELETE FROM demo.memo WHERE body='second';"
docker exec db mysql -uroot -pdev -e "SELECT * FROM demo.memo;"
1
2
body
first


-v의 용도를 확인하는 실험입니다. container를 완전히 지우고, 같은 volume으로 새 container를 띄웁니다.

1
2
3
4
5
6
7
8
9
docker stop db && docker rm db     # container 삭제. 이름까지 사라진다

docker run -d \
  --name db \
  -e MYSQL_ROOT_PASSWORD=dev \
  -v mysql-data:/var/lib/mysql \
  mysql:8.4

docker exec db mysql -uroot -pdev -e "SELECT * FROM demo.memo;"   # logs로 준비 확인 후
1
2
body
first

새 container인데 이전 실습의 내용이 그대로 있습니다. dbdㅢ 데이터가 container가 아니라 volume에 있었기 때문입니다. 남겨야 할 데이터는 -v로 container 밖에 둔다,이고 volume의 종류와 동작 원리는 7편에서 다룹니다.

DB, Redis, 메시지 큐 같은 인프라를 로컬에 직접 설치하지 않고 이렇게 띄웠다 지우는 것이 Docker의 대표적인 실용 가치입니다. SQL 자체와 MySQL 운영 기본은 MySQL 해부에서 따로 다룹니다.

실습을 마치면 정리합니다.

1
2
docker stop db && docker rm db
docker volume rm mysql-data      # 실험용 데이터까지 삭제

다음 편에서 여기서 친 run 명령들을 파일 하나로 옮기는 Docker Compose를 다룹니다.

다음 글: Docker 기초 (5) - Docker Compose: Commands and YAML

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.