Docker란?
Docker 란 어플리케이션 영역을 인프라와 분리해 가상의 실행환경을 제공해주는 오픈소스 플랫폼이다.
흔히 Docker 엔진 설치와 Docker 명령어를 통해 이미 패키징된 어플리케이션을 가져와 가상의 환경에서 빠르고 편하게 관리할 수 있다.
이번 스터디에서는 단순 명령어로 Wrapping 된 Container를 사용하기 위해서 어떤 기술이 필요한지, 실제 물리 서버와 가상환경을 어떻게 격리한 것인지 조금 더 자세히 알아봤다.
Container란?
Container를 어떻게 격리하는 것인지 알기에 앞서, Container가 무엇인지 알아야 뭐를 격리한건지 알 수 있다.
Docker에서는 가상의 실행환경을 Container라고 부른다.
- 좀 더 정확히 표현하는 용어는 '컨테이너화된 프로세스(Containerized Process)' 이다
- Docker 플랫폼이 설치된 곳이라면 Container로 묶인 애플리케이션을 어디서든 실행할 수 있는 장점을 가진다.
즉 여기서 Docker를 통해 격리된 것은 특정 어플리케이션에 대한 프로세스들이라고 할 수 있다.
Container 격리 대상
위의 이야기를 종합해보면 격리되어야 하는 목록은 아래와 같다.
- 특정 어플리케이션 실행을 위한 프로세스들
- 해당 프로세스들이 사용하는 파일 등 디스크
- 해당 프로세스들이 사용하는 자원(Resource)
- 해당 프로세스들이 사용하는 네트워크
이 외에도 세부적으로 들어가면 권한이나 여러가지 격리해야하는 대상들이 다양하게 존재한다.
격리해야하는 대상을 파악했으므로, 해당 대상에 대해서 좀 더 상세히 알아본다.
프로세스란?
운영체제 위에서 실행 중인 프로그램으로 프로그램 명령어와 데이터들이 메모리에 올라오고 실행 중 또는 실행 대기 중인 상태를 의미한다.
- 출처 : 우아한테크 Process vs Thread : https://youtu.be/DmZnOg5Ced8?si=N3E1yhJjobkXl-8I
Linux 프로세스 관련 명령어
// ps: 현재 실행 중인 프로세스 목록을 출력
ubuntu@MyServer:~$ ps -ef
UID PID PPID C STIME TTY TIME CMD
root 1 0 0 19:13 ? 00:00:03 /sbin/init
root 2 0 0 19:13 ? 00:00:00 [kthreadd]
root 3 2 0 19:13 ? 00:00:00 [rcu_gp]
root 4 2 0 19:13 ? 00:00:00 [rcu_par_gp]
...
// top 및 htop: 실시간으로 시스템 자원 사용 현황과 프로세스 상태를 모니터링
ubuntu@MyServer:~$ top
top - 20:10:22 up 57 min, 1 user, load average: 0.00, 0.00, 0.00
Tasks: 98 total, 1 running, 97 sleeping, 0 stopped, 0 zombie
%Cpu(s): 0.0 us, 0.0 sy, 0.0 ni,100.0 id, 0.0 wa, 0.0 hi, 0.0 si, 0.0 st
MiB Mem : 1901.4 total, 919.3 free, 173.1 used, 808.9 buff/cache
MiB Swap: 0.0 total, 0.0 free, 0.0 used. 1553.8 avail Mem
PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND
1 root 20 0 101940 12796 8316 S 0.0 0.7 0:03.18 systemd
2 root 20 0 0 0 0 S 0.0 0.0 0:00.00 kthreadd
3 root 0 -20 0 0 0 I 0.0 0.0 0:00.00 rcu_gp
...
// pgrep: 특정 프로세스를 검색
ubuntu@MyServer:~$ pgrep bash
2494
ubuntu@MyServer:~$ ps -ef | grep bash
ubuntu 2494 2493 0 19:15 pts/0 00:00:00 -bash
ubuntu 2544 2494 0 20:10 pts/0 00:00:00 grep --color=auto bash
// pstree: 프로세스 트리를 시각적으로 확인
ubuntu@MyServer:~$ pstree
systemd─┬─acpid
├─2*[agetty]
├─amazon-ssm-agen───7*[{amazon-ssm-agen}]
├─chronyd───chronyd
├─cron
├─dbus-daemon
...
/proc 파일 시스템
Linux는 모든 것을 파일로 취급해 관리하는데, 프로세스도 예외가 아니다.
특히, /proc 파일 시스템은 Linux 커널이 동적으로 생성하는 가상 파일 시스템으로, 시스템의 상태와 프로세스 관련 정보를 실시간으로 제공한다. 이 디렉토리 안에는 시스템 정보뿐만 아니라 개별 프로세스에 대한 정보도 포함되어 있다.
하드웨어 정보
- /proc/cpuinfo: CPU에 대한 상세 정보를 제공
- /proc/meminfo: 메모리 사용 현황을 제공
- /proc/uptime: 시스템 가동 시간을 표시
- /proc/loadavg: 시스템 부하 상태 표시
- /proc/version: 커널 버전 정보가 포함
- /proc/filesystems: 커널이 인식하고 있는 파일 시스템의 목록을 제공
- /proc/partitions: 시스템에서 인식된 파티션 정보를 제공
프로세스 정보
# sleep 명령어 수행
ubuntu@MyServer:~$ sleep 10000 &
[1] 2645
# 프로세스 ID 확인
ubuntu@MyServer:~$ pgrep sleep
2645
# 프로세스 ID로 생성된 /proc 디렉토리 정보
ubuntu@MyServer:~$ ls -rlt /proc/2645
total 0
-r--r--r-- 1 ubuntu ubuntu 0 Aug 28 20:19 status
-r--r--r-- 1 ubuntu ubuntu 0 Aug 28 20:19 cmdline
lrwxrwxrwx 1 ubuntu ubuntu 0 Aug 28 20:19 cwd -> /home/ubuntu
lrwxrwxrwx 1 ubuntu ubuntu 0 Aug 28 20:20 exe -> /usr/bin/sleep
-r-------- 1 ubuntu ubuntu 0 Aug 28 20:20 environ
-r--r--r-- 1 ubuntu ubuntu 0 Aug 28 20:20 maps
-r--r--r-- 1 ubuntu ubuntu 0 Aug 28 20:22 wchan
-rw-r--r-- 1 ubuntu ubuntu 0 Aug 28 20:22 uid_map
-rw-rw-rw- 1 ubuntu ubuntu 0 Aug 28 20:22 timerslack_ns
-r--r--r-- 1 ubuntu ubuntu 0 Aug 28 20:22 timers
-rw-r--r-- 1 ubuntu ubuntu 0 Aug 28 20:22 timens_offsets
dr-xr-xr-x 3 ubuntu ubuntu 0 Aug 28 20:22 task
-r-------- 1 ubuntu ubuntu 0 Aug 28 20:22 syscall
...
# 프로세스 명령어 및 인자 확인
ubuntu@MyServer:~$ cat /proc/$(pgrep sleep)/cmdline ; echo
sleep10000
# 프로세스의 현재 작업 디렉터리 확인
ubuntu@MyServer:~$ ls -l /proc/$(pgrep sleep)/cwd
lrwxrwxrwx 1 ubuntu ubuntu 0 Aug 28 20:19 /proc/2645/cwd -> /home/ubuntu
# 실행 중인 바이너리 경로 확인
ubuntu@MyServer:~$ ls -l /proc/$(pgrep sleep)/exe
lrwxrwxrwx 1 ubuntu ubuntu 0 Aug 28 20:20 /proc/2645/exe -> /usr/bin/sleep
# 프로세스의 환경 변수 확인
ubuntu@MyServer:~$ cat /proc/$(pgrep sleep)/environ ; echo
SHELL=/bin/bashPWD=/home/ubuntuLOGNAME=ubuntuXDG_SESSION_TYPE=ttyMOTD_SHOWN=pam .....
# 프로세스의 메모리 맵 확인
ubuntu@MyServer:~$ cat /proc/$(pgrep sleep)/maps
5bb6a298a000-5bb6a298c000 r--p 00000000 103:01 1610 /usr/bin/sleep
5bb6a298c000-5bb6a2990000 r-xp 00002000 103:01 1610 /usr/bin/sleep
5bb6a2990000-5bb6a2991000 r--p 00006000 103:01 1610 /usr/bin/sleep
...
# 프로세스 상태 정보 확인
ubuntu@MyServer:~$ cat /proc/$(pgrep sleep)/status
Name: sleep
Umask: 0002
State: S (sleeping)
Tgid: 2645
Ngid: 0
Pid: 2645
PPid: 2633
TracerPid: 0
Uid: 1000 1000 1000 1000
Gid: 1000 1000 1000 1000
FDSize: 256
Groups: 4 20 24 25 27 29 30 44 46 119 120 1000
NStgid: 2645
...
Linux Container 격리
위에서 알아본 프로세스의 다양한 정보를 토대로 Container 격리 기술을 하나씩 테스트 해보자.
이게 돼요? 도커 없이 컨테이너 만들기 / if(kakao)2022
https://youtu.be/mSD88FuST80?si=YFMLwVcCZJVYLG9A
해당 자료를 토대로 스터디를 진행하였다.
아래의 그림은 프로세스 격리 기술이 어떤식으로 발전해왔는지를 보여준다.
chroot
리눅스의 chroot(change root) 명령어는 1979년, 유닉스 운영체제의 버전 7에서 처음 도입되었다고 함
- chroot는 새로운 루트 디렉토리(/ 디렉토리)를 지정하여, 프로그램이 이 디렉토리 밖의 파일 시스템에 접근하지 못하게 하는 기능을 제공한다.
- 즉, user 디렉터리를 user 프로세스에게 root 디렉터리를 속이는 명령어라고 할 수 있다.
# root 계정으로 진행
root@MyServer:/# mkdir -p /tmp/fake_root_dir
root@MyServer:/# cd /tmp/fake_root_dir
# chroot [OPTION] NEW_ROOT [COMMAND [ARG]...]
# chroot명령어로 /tmp/fake_root_dir 디렉토리를 root디렉토리로 변경하도록 설정
# root디렉토리를 변경하면서 bash 쉘을 실행
root@MyServer:/tmp/fake_root_dir# chroot /tmp/fake_root_dir /bin/bash
chroot: failed to run command ‘/bin/bash’: No such file or directory
# 변경된 root디렉토리인 /tmp/fake_root_dir에 bash 쉘 명령어가없기 때문에 실행할 수가 없는 상황
# /tmp/fake_root_dir디렉토리에 bash쉘을 넣어줘야 정상 동작한다
root@MyServer:/tmp/fake_root_dir# ls -rlt /bin/bash
-rwxr-xr-x 1 root root 1396520 Mar 14 20:31 /bin/bash
root@MyServer:/tmp/fake_root_dir#
root@MyServer:/tmp/fake_root_dir# mkdir -p /tmp/fake_root_dir/bin
root@MyServer:/tmp/fake_root_dir# cp /bin/bash /tmp/fake_root_dir/bin/bash
root@MyServer:/tmp/fake_root_dir#
root@MyServer:/tmp/fake_root_dir# ls -rlt /tmp/fake_root_dir/bin/bash
-rwxr-xr-x 1 root root 1396520 Aug 31 23:02 /tmp/fake_root_dir/bin/bash
root@MyServer:/tmp/fake_root_dir# chroot /tmp/fake_root_dir /bin/bash
chroot: failed to run command ‘/bin/bash’: No such file or directory
# 또한 bash 쉘이 실행될 때 필요한 의존성 라이브러리가 필요하기 때문에
# 필요한 라이브러리도 같이 복사해줌
root@MyServer:/tmp/fake_root_dir# ldd /bin/bash
linux-vdso.so.1 (0x00007fffd566f000)
libtinfo.so.6 => /lib/x86_64-linux-gnu/libtinfo.so.6 (0x000071808720c000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x0000718086e00000)
/lib64/ld-linux-x86-64.so.2 (0x00007180873a7000)
root@MyServer:/tmp/fake_root_dir# mkdir -p /tmp/fake_root_dir/{lib64,lib/x86_64-linux-gnu}
root@MyServer:/tmp/fake_root_dir# cp /lib/x86_64-linux-gnu/libc.so.6 /tmp/fake_root_dir/lib/x86_64-linux-gnu/
root@MyServer:/tmp/fake_root_dir# cp /lib64/ld-linux-x86-64.so.2 /tmp/fake_root_dir/lib64
root@MyServer:/tmp/fake_root_dir# cp /lib/x86_64-linux-gnu/libtinfo.so.6 /tmp/fake_root_dir/lib/x86_64-linux-gnu/
root@MyServer:/tmp/fake_root_dir#
root@MyServer:/tmp/fake_root_dir# tree /tmp/fake_root_dir/
/tmp/fake_root_dir/
├── bin
│ └── bash
├── lib
│ └── x86_64-linux-gnu
│ ├── libc.so.6
│ └── libtinfo.so.6
└── lib64
└── ld-linux-x86-64.so.2
4 directories, 4 files
root@MyServer:/tmp/fake_root_dir# chroot /tmp/fake_root_dir /bin/bash
bash-5.1#
bash-5.1# pwd
/
bash 쉘 실행까지는 완료했으나 ls, ps 등의 명령어는 변경한 fake root 디렉토리에 없기 때문에 실행할 수가 없다.
bash-5.1# ls
bash: ls: command not found
bash-5.1# ps
bash: ps: command not found
ls 와 ps 명령어도 똑같이 bash 명령어 처럼 명령어 파일과 의존성 파일을 같이 복사해준다.
root@MyServer:/tmp/fake_root_dir# ldd /usr/bin/ps
linux-vdso.so.1 (0x00007fff88574000)
libprocps.so.8 => /lib/x86_64-linux-gnu/libprocps.so.8 (0x00007620e1f17000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007620e1c00000)
libsystemd.so.0 => /lib/x86_64-linux-gnu/libsystemd.so.0 (0x00007620e1e50000)
/lib64/ld-linux-x86-64.so.2 (0x00007620e1f9c000)
liblzma.so.5 => /lib/x86_64-linux-gnu/liblzma.so.5 (0x00007620e1bd5000)
libzstd.so.1 => /lib/x86_64-linux-gnu/libzstd.so.1 (0x00007620e1b06000)
liblz4.so.1 => /lib/x86_64-linux-gnu/liblz4.so.1 (0x00007620e1e2e000)
libcap.so.2 => /lib/x86_64-linux-gnu/libcap.so.2 (0x00007620e1afb000)
libgcrypt.so.20 => /lib/x86_64-linux-gnu/libgcrypt.so.20 (0x00007620e19bd000)
libgpg-error.so.0 => /lib/x86_64-linux-gnu/libgpg-error.so.0 (0x00007620e1997000)
root@MyServer:/tmp/fake_root_dir# ldd /usr/bin/ls
linux-vdso.so.1 (0x00007ffc28c68000)
libselinux.so.1 => /lib/x86_64-linux-gnu/libselinux.so.1 (0x00007ba4eb495000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ba4eb200000)
libpcre2-8.so.0 => /lib/x86_64-linux-gnu/libpcre2-8.so.0 (0x00007ba4eb169000)
/lib64/ld-linux-x86-64.so.2 (0x00007ba4eb4ed000)
# ps명령어 및 의존성 파일 복사
cp /usr/bin/ps /tmp/fake_root_dir/bin/
cp /lib/x86_64-linux-gnu/libprocps.so.8 /tmp/fake_root_dir/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libc.so.6 /tmp/fake_root_dir/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libsystemd.so.0 /tmp/fake_root_dir/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/liblzma.so.5 /tmp/fake_root_dir/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libzstd.so.1 /tmp/fake_root_dir/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/liblz4.so.1 /tmp/fake_root_dir/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libcap.so.2 /tmp/fake_root_dir/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libgcrypt.so.20 /tmp/fake_root_dir/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libgpg-error.so.0 /tmp/fake_root_dir/lib/x86_64-linux-gnu/
cp /lib64/ld-linux-x86-64.so.2 /tmp/fake_root_dir/lib64/
# ls명령어 및 의존성 파일 복사
cp /usr/bin/ls /tmp/fake_root_dir/bin/
cp /lib/x86_64-linux-gnu/libselinux.so.1 /tmp/fake_root_dir/lib/x86_64-linux-gnu/
cp /lib/x86_64-linux-gnu/libpcre2-8.so.0 /tmp/fake_root_dir/lib/x86_64-linux-gnu/
# 명령어 수행
# ls 명령어는 정상 수행
root@MyServer:/tmp/fake_root_dir# chroot /tmp/fake_root_dir /bin/bash
bash-5.1# ls -rlt /
total 12
drwxr-xr-x 3 0 0 4096 Aug 31 14:05 lib
drwxr-xr-x 2 0 0 4096 Aug 31 14:06 lib64
drwxr-xr-x 2 0 0 4096 Aug 31 14:17 bin
# ps 명령어는 수행이 되지 않음
bash-5.1# ps
Error, do this: mount -t proc proc /proc
ls 명령어는 정상 수행 되었으나 ps 명령어는 수행되지 않는다.
이유는 앞서 살펴본 /proc 디렉토리에 프로세스에 대한 정보를 기록하고 가져오기 때문이다.
이를 해결하기 위해 mkdir 명령어와 mount 명령어를 사용해 /proc 디렉토리를 연결해주도록 한다.
# mount명령어 및 의존성 파일 복사
ldd /usr/bin/mount;
cp /usr/bin/mount /tmp/fake_root_dir/bin/;
cp /lib/x86_64-linux-gnu/{libmount.so.1,libc.so.6,libblkid.so.1,libselinux.so.1,libpcre2-8.so.0} /tmp/fake_root_dir/lib/x86_64-linux-gnu/;
cp /lib64/ld-linux-x86-64.so.2 /tmp/fake_root_dir/lib64/;
# mkdir명령어 및 의존성 파일 복사
ldd /usr/bin/mkdir;
cp /usr/bin/mkdir /tmp/fake_root_dir/bin/;
cp /lib/x86_64-linux-gnu/{libselinux.so.1,libc.so.6,libpcre2-8.so.0} /tmp/fake_root_dir/lib/x86_64-linux-gnu/;
cp /lib64/ld-linux-x86-64.so.2 /tmp/fake_root_dir/lib64/;
# chroot 후 /proc 디렉토리 생성 및 마운트
bash-5.1# mkdir /proc
bash-5.1# mount -t proc proc /proc
bash-5.1# ls -rlt /proc
total 0
lrwxrwxrwx 1 0 0 0 Aug 31 14:24 thread-self -> 2703/task/2703
lrwxrwxrwx 1 0 0 0 Aug 31 14:24 self -> 2703
-r--r--r-- 1 0 0 0 Aug 31 14:24 zoneinfo
-r--r--r-- 1 0 0 0 Aug 31 14:24 vmstat
...
...
dr-xr-xr-x 9 0 0 0 Aug 31 14:24 1
bash-5.1#
bash-5.1# mount -t proc
proc on /proc type proc (rw,relatime)
# ps 명령어 정상 수행
bash-5.1# ps -ef
UID PID PPID C STIME TTY TIME CMD
0 1 0 0 13:35 ? 00:00:03 /sbin/init
0 2 0 0 13:35 ? 00:00:00 [kthreadd]
0 3 2 0 13:35 ? 00:00:00 [rcu_gp]
0 4 2 0 13:35 ? 00:00:00 [rcu_par_gp]
0 5 2 0 13:35 ? 00:00:00 [slub_flushwq]
0 6 2 0 13:35 ? 00:00:00 [netns]
0 8 2 0 13:35 ? 00:00:00 [kworker/0:0H-events_highpri]
0 9 2 0 13:35 ? 00:00:00 [kworker/0:1-events]
0 11 2 0 13:35 ? 00:00:00 [mm_percpu_wq]
0 12 2 0 13:35 ? 00:00:00 [rcu_tasks_rude_kthread]
...
...
(참고자료) /proc를 사용하는 또 다른 예시
/proc를 통해 프로세스의 정보를 얻기 때문에 특히 컨테이너로 기동한 경우 이런 디렉토리를 마운트해야 정상적으로 동작하는 경우가 있다.
예를 들면 zabbix 모니터링 Linux Agent를 컨테이너로 기동했을 경우 컨테이너에서는 host의 프로세스나 기타 정보를 알 수 없기 때문에 아래와 같이 /proc 경로를 마운트해줄 필요가 생긴다.
# docker compose 예시
services:
zabbix-agent:
image: zabbix/zabbix-agent2:latest
container_name: zabbix-agent
restart: unless-stopped
ports:
- 10050:10050
environment:
- ZBX_HOSTNAME=TEST
- ZBX_SERVER_HOST=192.168.0.100
- ZBX_SERVER_ACTIVE=192.168.0.100
# 컨테이너에서 Host 정보를 가져오기 위한 추가 마운트 및 설정
volumes:
- /:/rootfs:ro
- /var/run:/var/run
- /proc:/host/proc:ro
- /sys:/host/sys:ro
- /sys/class/net:/host/sys/class/net:ro
privileged: true
network_mode: host
container 추출 파일을 chroot에서 사용
이와 같은 원리로 다른 사람이 작성한 container 이미지를 chroot를 이용해서 사용해보자
container 이미지란 격리된 환경에서 프로세스가 실행되는데 필요한 모든 파일을 패키징한 묶음이기 때문에,
앞서 수행한 bash, ps, ls와 같은 명령어 파일과 같이 container 목적에 필요한 명령어와 의존성 라이브러리를 모두 가지고 있다.
# docker 설치
# root 권한으로 설치하고 사용하는 것은 보안상 권장되지 않으나
# 빠른 실습을 위해 root 계정으로 작업
curl -fsSL https://get.docker.com | sh
# nginx 컨테이너 이미지 다운로드 후 chroot할 디렉토리에 패키징 내용 추출
root@MyServer:~# mkdir /root/nginx-root
root@MyServer:~#
root@MyServer:~# docker export $(docker create nginx) | tar -C /root/nginx-root -xvf -;
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
e4fff0779e6d: Pull complete
2a0cb278fd9f: Pull complete
7045d6c32ae2: Pull complete
03de31afb035: Pull complete
0f17be8dcff2: Pull complete
14b7e5e8f394: Pull complete
23fa5a7b99a6: Pull complete
Digest: sha256:447a8665cc1dab95b1ca778e162215839ccbb9189104c79d7ec3a81e14577add
Status: Downloaded newer image for nginx:latest
.dockerenv
bin
boot/
dev/
dev/console
dev/pts/
dev/shm/
docker-entrypoint.d/
docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
...
...
# nginx 기동에 필요한 여러 파일들이 추출된 것을 확인
root@MyServer:~# tree -L 1 /root/nginx-root/
/root/nginx-root/
├── bin -> usr/bin
├── boot
├── dev
├── docker-entrypoint.d
├── docker-entrypoint.sh
├── etc
├── home
├── lib -> usr/lib
├── lib64 -> usr/lib64
├── media
├── mnt
├── opt
├── proc
├── root
├── run
├── sbin -> usr/sbin
├── srv
├── sys
├── tmp
├── usr
└── var
# 기존 host 쉘에서 nginx가 PATH에 없어 인식하지 못했지만
# chroot 후에는 root 기준 경로가 변경되어 nginx를 실행 가능
root@MyServer:~# nginx
Command 'nginx' not found, but can be installed with:
apt install nginx-core # version 1.18.0-6ubuntu14.4, or
apt install nginx-extras # version 1.18.0-6ubuntu14.4
apt install nginx-light # version 1.18.0-6ubuntu14.4
root@MyServer:~#
root@MyServer:~# chroot /root/nginx-root /bin/sh
#
# nginx -g "daemon off;"
nginx 수행 후 또 다른 터미널에서 nginx 프로세스와 기본 포트인 80 포트로 접속을 테스트해볼 수 있다.
참고자료
- Docker Engine License : https://docs.docker.com/engine/#licensing
- 이게 돼요? 도커 없이 컨테이너 만들기 / if(kakao)2022 : https://youtu.be/mSD88FuST80?si=YFMLwVcCZJVYLG9A
- 우아한테크 Process vs Thread : https://youtu.be/DmZnOg5Ced8?si=N3E1yhJjobkXl-8I