네트워크 생성(개발, 운영)
$ docker network create dev-network
0f2960e0ead0eb7ad31fc6b75e5450ff7ead9e704aac41278fa6b26ac9e8d58e
$ docker network create prod-network
8517dea65d5448b6d81b7d3e4658de43c923740ebc29116e38183c279f364114
컨테이너 생성(개발, 운영)
$ docker run -d --name dev-db --network dev-network -p 3306:3306 -e MYSQL_ROOT_PASSWORD=1234 mysql:5.7
6bee6724625debc9230546f87022963988a8317929c3e29b67f2eccc3d8fc39e
$ docker run -d --name prod-db --network prod-network -p 3307:3306 -e MYSQL_ROOT_PASSWORD=1234 mysql:5.7
fec3ad1f7613767511345bf45205982122c5fa208b5ddae9303801f235310a0c
개발 환경, 운영 환경 접속 후 데이터베이스 생성 및 데이터 삽입
# db 생성
$ docker exec dev-db mysql -uroot -p1234 -e "create database appdb;"
$ docker exec prod-db mysql -uroot -p1234 -e "create database appdb;"
# 테이블 생성
$ docker exec dev-db mysql -uroot -p1234 -e "create table appdb.users (id INT, name VARCHAR(50));"
$ docker exec prod-db mysql -uroot -p1234 -e "create table appdb.users (id INT, name VARCHAR(50));"
# 테이블에 데이터 삽입
$ docker exec dev-db mysql -uroot -p1234 -e "insert into appdb.users values (1, 'from dev');"
$ docker exec prod-db mysql -uroot -p1234 -e "insert into appdb.users values (1, 'from prod');"
확인
**$ docker exec dev-db mysql -uroot -p1234 -e "select * from appdb.users;"**
mysql: [Warning] Using a password on the command line interface can be insecure.
id name
1 from dev
**$ docker exec prod-db mysql -uroot -p1234 -e "select * from appdb.users;"**
mysql: [Warning] Using a password on the command line interface can be insecure.
id name
1 from prod
네트워크 테스트
**/ # ping -c 3 dev-db**
PING dev-db (172.23.0.2): 56 data bytes
64 bytes from 172.23.0.2: seq=0 ttl=64 time=0.336 ms
64 bytes from 172.23.0.2: seq=1 ttl=64 time=0.155 ms
64 bytes from 172.23.0.2: seq=2 ttl=64 time=0.144 ms
--- dev-db ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.144/0.211/0.336 ms
# 운영환경에 보내면 다른 네트워크에 있으므로 당연히 가지 않음
**/ # ping -c 3 prod-db**
ping: bad address 'prod-db'
ARG: 빌드 변수 정의
개발 환경과 운영 환경의 빌드 베이스 이미지를 다르게 하고 싶을 때
개발 환경 빌드
**$ vi Dockerfile**
ARG BASE_VERSION=9
FROM public.ecr.aws/docker/library/rockylinux:
${BASE_VERSION}
RUN yum install -y httpd
**$ docker build -t myapp:dev .**
운영 환경 빌드(베이스 이미지 8로 지정)
$ docker build --build-arg BASE_VERSION=8 -t myapp:prod .
확인
**$ docker images | grep myapp**
myapp prod c756cc928ed4 About a minute ago 323MB
myapp dev 3f788429af27 3 hours ago 234MB
LABEL: 이미지에 메타데이터 추가
**$ vi Dockerfile**
FROM rockylinx:9
LABEL maintainer="minseop@example.com" \
version="1.0" \
description="라파 웹서버" \
team="인프라팀"
빌드
$ docker build -t myapp:1.0 .
인스펙트
$ docker inspect myapp:1.0 | grep -A 5 Labels
"Labels": {
"description": "라파 웹서버",
"maintainer": "dltnals1210@gmail.com",
"team": "인프라팀",
"version": "1.0"
},
FROM rockylinux:9
RUN yum install -y httpd
COPY index.html /var/www/html/index.html
HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
CMD curl -f http://localhost/ || exit1
CMD ["/usr/sbin/httpd", "-DFOREGROUND"]
docker build -t webapp:health .
docker run -d --name web webapp:health
docker exec web rm -f /var/www/html/index.html
watch -n1 docker ps
**$ docker exec web pkill httpd**
OCI runtime exec failed: exec failed: unable to start container process: exec: "pkill": executable file not found in $PATH: unknown
pkill 기본적으로 설치 안돼있음
$ docker exec web yum install -y procps-ng
설치
$ docker exec web pkill httpd
web 컨테이너의 httpd 프로세스 kill
web 컨테이너의 CMD(메인 프로세스, PID 1)인 httpd 프로세스가 종료됐기 때문에 컨테이너도 종료됨
**$ docker ps**
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
새로 컨테이너 생성
$ docker run -d --name web-test webapp:health
컨테이너 내부 프로세스 확인
**$ docker top web-test**
UID PID PPID C STIME TTY TIME CMD
root 65108 65083 0 04:06 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND
48 65129 65108 0 04:06 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND
48 65130 65108 0 04:06 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND
48 65131 65108 0 04:06 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND
48 65132 65108 0 04:06 ? 00:00:00 /usr/sbin/httpd -DFOREGROUND
원하는 정보가 다 나오지 않음
**$ docker exec web-test ps**
OCI runtime exec failed: exec failed: unable to start container process: exec: "ps": executable file not found in $PATH: unknown
ps -ef 로 확인하려고 하면 기본적으로 ps 가 설치 되어 있지 않음
**$ docker exec web-test cat /proc/1/cmdline**
/usr/sbin/httpd-DFOREGROUND
/proc/1/cmdline
: “컨테이너의 PID 1 프로세스가 실행 중인 실제 명령어”를 보여주는 파일
: 즉, “이 컨테이너가 어떤 명령으로 살아있는가”를 정확히 알려주는 리눅스 내부 스냅샷
**$ vi myapp.sh**
#!/bin/bash
echo "현재 사용자: $(whoami)"
echo "현재 UID: $(id -u)"
sleep 1000
실행 권한 부여
$ chmod +x myapp.sh
도커 파일 작성
**$ vi Dockerfile**
FROM rockylinux:9
RUN yum install -y httpd
# 1. 일반 사용자 생성
RUN useradd -m -s /bin/bash appuser
# 2. 애플리케이션 디렉터리 권한 추가
WORKDIR /app
COPY myapp.sh /app/
RUN chown appuser:appuser /app
USER appuser
CMD ["./myapp.sh"]
이미지 빌드
$ docker build -t userapp .
컨테이너 생성 및 실행
$ docker run -d --name user-app userapp
컨테이너 실행시 myapp.sh 실행, 결과 확인
$ docker logs user-app
현재 사용자: appuser
현재 UID: 1000
로그로 확인
디렉터리, 파일 생성 및 압축
$ mkdir add
$ cd add/
$ echo "hello docker" > test.txt
$ ls
test.txt
$ tar -czf test.tar.gz test.txt
$ ls
test.tar.gz test.txt
도커파일 복사
**$ vi Dockerfile.copy**
FROM rockylinux:9
WORKDIR /app
COPY test.txt /app/
COPY test.tar.gz /app/
CMD ["ls", "-la", "/app"]
**$ vi Dockerfile.add**
FROM rockylinux:9
WORKDIR /app
ADD test.txt /app/
ADD test.tar.gz /app/
CMD ["ls", "-la", "/app"]
카피 빌드
$ docker build -f Dockerfile.copy -t test:copy .
Dockerfile 이 아니므로 파일 따로 지정(-f)
카피 실행
**$ docker run --rm test:copy**
total 8
drwxr-xr-x. 1 root root 25 Oct 21 04:30 .
drwxr-xr-x. 1 root root 6 Oct 21 04:31 ..
-rw-r--r--. 1 root root 137 Oct 21 04:26 test.tar.gz
-rw-r--r--. 1 root root 13 Oct 21 04:25 test.txt
add 빌드 및 실행
$ docker build -f Dockerfile.add -t test:add .
$ docker run --rm test:add
total 4
drwxr-xr-x. 1 root root 22 Oct 21 04:34 .
drwxr-xr-x. 1 root root 6 Oct 21 04:34 ..
-rw-r--r--. 1 1000 1000 13 Oct 21 04:25 test.txt
test.tar.gz 파일이 없는 것을 확인 가능
왜?
ADD 는 압축을 알아서 풀어줌
COPY vs. ADD
실무에선 COPY 를 권장함
도커 이미지(image)
: “여러 개의 읽기 전용 파일시스템 스냅샷(=레이어)”이 겹쳐진 것
각 Dockerfile 명령어(FROM, RUN, COPY, ADD, …)가 한 줄씩 실행될 때마다
그 결과로 생긴 파일시스템의 변경사항(diff) 이 하나의 레이어(layer) 로 저장됨
레이어는 수정될 수 없음(읽기 전용)
즉, 한 번 생성되면 삭제 불가
도커는 UnionFS (예: OverlayFS, AUFS) 라는 파일시스템을 사용해서 이 여러 개의 레이어를 하나의 통합된 파일시스템처럼 보여줌
[ 읽기 전용 레이어들 (이미지 레이어) ]
├── Base layer (ubuntu)
├── apt-get install layer
├── COPY index.html layer
[ 쓰기 가능한 레이어 (container layer) ]
└── 컨테이너 실행 시 생성되는 overlay
즉, 컨테이너가 실행될 때는
이미지의 여러 “읽기 전용 레이어” 위에
“쓰기 가능한 새로운 레이어(컨테이너 레이어)”가 하나 더 생긴다
| 구분 | 설명 | 특징 |
|---|---|---|
| 이미지 레이어 | Dockerfile 빌드 과정에서 생긴 읽기 전용 파일시스템 | 변경 불가, 여러 컨테이너가 공유 |
| 컨테이너 레이어 | 컨테이너 실행 시 생기는 쓰기 가능한 최상위 레이어 | 컨테이너 종료 시 삭제 가능 |
| 개념 | 설명 |
|---|---|
| 레이어 | 파일시스템 변경(diff)을 저장한 단위 |
| 이미지 | 여러 개의 읽기 전용 레이어들의 집합 |
| 컨테이너 | 이미지 레이어 + 쓰기 가능한 레이어 |
| 파일시스템 | OverlayFS로 여러 레이어를 하나로 합쳐서 보여줌 |
| 캐싱 효과 | 빌드 시 이전 단계가 동일하면 해당 레이어 재사용 |
RUN apt-get update
RUN apt-get install -y a
RUN apt-get install -y b
RUN apt-get install -y c
RUN apt-get install -y d
RUN apt-get install -y e
6개 레이어
RUN apt-get update && apt get install -y a b c d e
1개 레이어
실습
mkdir layer
cd layer
echo "file1" > file1.txt
echo "file1" > file2.txt
echo "file1" > file3.txt
vi Dockerfile.bad
FROM ubuntu
RUN apt-get update
RUN apt-get install -y curl
RUN apt-get install -y wget
RUN apt-get install -y vim
RUN apt-get install -y git
COPY file1.txt /app/
COPY file2.txt /app/
COPY file3.txt /app/
RUN apt-get install -y python3
RUN apt-cache policy python3 > /tmp/package-info.txt
RUN cat /tmp/package-info.txt
RUN rm /tmp/package-info.txt
CMD ["/bin/bash"]
docker build -f Dockerfile.bad -t test:bad .
docker images test:bad
vi Dockerfile.good
FROM ubuntu
RUN apt-get update && apt-get install -y \
curl \
wget \
vim \
git \
python3 \
&& rm -rf /var/lib/apt/lists/*
COPY file*.txt /app/
RUN apt-cache policy python3 > /tmp/package-info.txt \
&& cat /tmp/package-info.txt \
&& rm /tmp/package-info.txt
CMD ["/bin/bash"]
docker build -f Dockerfile.good -t test:good .
docker images test:good
docker history test:good
docker run -it --name test-bad test:bad bash
ls -lh /var/lib/apt/lists/
du -sh /var/lib/apt/lists/
: 하나의 Dockerfile 안에서 여러 개의 FROM 단계를 사용하여 빌드용 환경과 실행용 환경을 분리하는 기법
⇒ 이렇게 “중간 빌드 환경”을 버리고 최종 실행용 이미지만 남기는 것이 핵심
$ cd ~
$ mkdir python-multistage
$ cd python-multistage/
**$ vi app.py**
from flask import Flask
app = Flask(__name__)
@app.route('/')
def hello():
return '<h1>Multi-Stage Build</h1>'
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000)
설치가 필요한 패키지 목록 작성
# 설치가 필요한 패키지 목록
$ vi requirements.txt
# 운영 환경
Flask==3.1.0
# 개발 전용 도구
pytest==8.3.3
black==24.10.0
flake8==7.1.1
mypy=1.13.0
ipython==8.29.0
**$ vi requirements-prod.txt**
Flask==3.1.0
빌드하는 데에 필요한 모든 파일 준비 완료
**$ ls**
app.py requirements-prod.txt requirements.txt
도커파일 작성
**$ vi Dockerfile.single**
# 일반적인 방법
FROM python:3.11
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
빌드
$ docker build -f Dockerfile.single -t pyapp:single .
빌드된 이미지 확인
**$ docker images pyapp:single**
REPOSITORY TAG IMAGE ID CREATED SIZE
pyapp single d3128d664040 20 minutes ago 1.21GB
1.2GB → 크기가 큼
**$ docker history pyapp:single**
IMAGE CREATED CREATED BY SIZE COMMENT
d3128d664040 21 minutes ago CMD ["python" "app.py"] 0B buildkit.dockerfile.v0
<missing> 21 minutes ago EXPOSE [5000/tcp] 0B buildkit.dockerfile.v0
<missing> 21 minutes ago COPY app.py . # buildkit 185B buildkit.dockerfile.v0
<missing> 21 minutes ago RUN /bin/sh -c pip install --no-cache-dir -r… 103MB buildkit.dockerfile.v0
<missing> 21 minutes ago COPY requirements.txt . # buildkit 126B buildkit.dockerfile.v0
<missing> 21 minutes ago WORKDIR /app 0B buildkit.dockerfile.v0
<missing> 13 days ago CMD ["python3"] 0B buildkit.dockerfile.v0
<missing> 13 days ago RUN /bin/sh -c set -eux; for src in idle3 p… 36B buildkit.dockerfile.v0
<missing> 13 days ago RUN /bin/sh -c set -eux; wget -O python.ta… 64MB buildkit.dockerfile.v0
<missing> 13 days ago ENV PYTHON_SHA256=8d3ed8ec5c88c1c95f5e558612… 0B buildkit.dockerfile.v0
<missing> 13 days ago ENV PYTHON_VERSION=3.11.14 0B buildkit.dockerfile.v0
<missing> 13 days ago ENV GPG_KEY=A035C8C19219BA821ECEA86B64E628F8… 0B buildkit.dockerfile.v0
<missing> 13 days ago RUN /bin/sh -c set -eux; apt-get update; a… 17.9MB buildkit.dockerfile.v0
<missing> 13 days ago ENV LANG=C.UTF-8 0B buildkit.dockerfile.v0
<missing> 13 days ago ENV PATH=/usr/local/bin:/usr/local/sbin:/usr… 0B buildkit.dockerfile.v0
<missing> 21 months ago RUN /bin/sh -c set -ex; apt-get update; ap… 656MB buildkit.dockerfile.v0
<missing> 21 months ago RUN /bin/sh -c set -eux; apt-get update; a… 185MB buildkit.dockerfile.v0
<missing> 21 months ago RUN /bin/sh -c set -eux; apt-get update; a… 60.2MB buildkit.dockerfile.v0
<missing> 21 months ago # debian.sh --arch 'amd64' out/ 'trixie' '@1… 120MB debuerreotype 0.16
설치된 패키지 목록 확인
$ docker exec -it py-single bash
root@b637dc90d20f:/app# pip list
Package Version
----------------- -------
asttokens 3.0.0
black 24.10.0
blinker 1.9.0
click 8.3.0
decorator 5.2.1
executing 2.2.1
flake8 7.1.1
Flask 3.1.0
iniconfig 2.3.0
ipython 8.29.0
itsdangerous 2.2.0
jedi 0.19.2
Jinja2 3.1.6
MarkupSafe 3.0.3
matplotlib-inline 0.1.7
mccabe 0.7.0
mypy 1.13.0
mypy_extensions 1.1.0
packaging 25.0
parso 0.8.5
pathspec 0.12.1
pexpect 4.9.0
pip 24.0
platformdirs 4.5.0
pluggy 1.6.0
prompt_toolkit 3.0.52
ptyprocess 0.7.0
pure_eval 0.2.3
pycodestyle 2.12.1
pyflakes 3.2.0
Pygments 2.19.2
pytest 8.3.3
setuptools 79.0.1
stack-data 0.6.3
traitlets 5.14.3
typing_extensions 4.15.0
wcwidth 0.2.14
Werkzeug 3.1.3
wheel 0.45.1
root@b637dc90d20f:/app# which gcc
/usr/bin/gcc
root@b637dc90d20f:/app# exit
도커파일 작성
**$ vi Dockerfile.multi**
# Stage 1
FROM python:3.11 AS builder
WORKDIR /app
COPY requirements-prod.txt .
RUN pip install --user --no-cache-dir -r requirements-prod.txt
# Stage 2
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /root/.local /root/.local .
COPY app.py .
ENV PATH=/root/.local/bin:$PATH
EXPOSE 5000
CMD ["python", "app.py"]
“빌드 환경(Stage 1)”과 “실행 환경(Stage 2)”을 분리
최종 이미지를 작고, 깨끗하고, 빠르게 만드는 구조
Stage 1 : Builder
python:3.11 : 컴파일 도구·라이브러리 등이 포함된 풀 버전 Python 이미지.WORKDIR /app : 이후 모든 명령의 작업 디렉터리.COPY requirements-prod.txt . : 의존성 목록 복사.RUN pip install ... : 필요한 패키지를 /root/.local/ 아래에 설치.-no-cache-dir : pip 캐시 저장 방지 → 이미지 크기 축소. ⇒ 결과 : Stage 1 에 /root/.local/ 경로가 만들어지고, 거기에 패키지들이 설치됨.Stage 2 : Runtime
python:3.11-slim : 실행만 가능한 경량 이미지 (불필요한 빌드툴 없음).COPY --from=builder /root/.local /root/.local . → Stage 1 에서 설치한 패키지들을 가져옴. (결국 Stage 2 에는 Python 표준 + 의존 패키지 + app.py 만 남음)COPY app.py . : 애플리케이션 코드 복사.ENV PATH=/root/.local/bin:$PATH : pip가 설치한 실행 파일을 PATH에 추가.EXPOSE 5000 : Flask 웹 앱 포트 노출.CMD ["python", "app.py"] : 앱 실행 명령빌드
**$ docker build -f Dockerfile.multi -t pyapp:multi .
$ docker images pyapp:multi**
REPOSITORY TAG IMAGE ID CREATED SIZE
pyapp multi a93c345f7a60 39 seconds ago 129MB
컨테이너 생성
$ docker run -d -p 5001:5000 --name py-mulit pyapp:multi
오류남
$ docker logs py-mulit
Traceback (most recent call last):
File "/app/app.py", line 1, in <module>
from flask import Flask
ModuleNotFoundError: No module named 'flask'
도커 파일 모듈 탐색 경로 root 에서 usr 로 바꾸기(--user 사용x)
# Stage 1
FROM python:3.11 AS builder
WORKDIR /app
COPY requirements-prod.txt .
RUN pip install --no-cache-dir -r requirements-prod.txt
# Stage 2
FROM python:3.11-slim
WORKDIR /app
COPY --from=builder /usr/local /usr/local
COPY app.py .
EXPOSE 5000
CMD ["python", "app.py"]
정상 작동
**$ curl localhost:5001**
<h1>Multi-Stage Build</h1>
설치된 패키지 목록 확인
**$ docker exec -it py-multi bash**
**root@7d0fc1ba5291:/app# pip list**
Package Version
------------ -------
blinker 1.9.0
click 8.3.0
Flask 3.1.0
itsdangerous 2.2.0
Jinja2 3.1.6
MarkupSafe 3.0.3
pip 24.0
setuptools 79.0.1
Werkzeug 3.1.3
wheel 0.45.1
**root@7d0fc1ba5291:/app# which gcc
root@7d0fc1ba5291:/app# exit**
exit
싱글과 비교하면 확연히 적은 패키지 개수, gcc 또한 없음
⇒ 운영 환경이므로 개발 환경에서 필요한 패키지들이 필요없음