이번에는 docker 이미지 최적화에 대해서 설명하겠습니다.
이미지 최적화를 위해서는 다음의 3가지를 잘 활용하면 됩니다.
1. 레이어를 줄이기 위해서 다중 RUN 명령어는 하나의 RUN 명령어로 구성
2. 파일 복사와 라이브러리 Install 은 순서가 중요
3. 컴파일과 같은 작업은 Multistep build 를 이용
alpine linux 로 nginx 를 실행시기 위한 방법으로 다음과 같은 docker 이미지를 만들 수 있습니다.
먼저, nginx.conf 파일을 로컬 컴퓨터에 생성합니다.
$ vi nginx.conf
user www;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
access_log /var/log/nginx/access.log;
keepalive_timeout 3000;
server {
listen 80;
root /www;
index index.html index.htm;
server_name localhost;
client_max_body_size 32m;
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root /var/lib/nginx/html;
}
}
}
다음은 간단한 index.html 입니다.
$ vi index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>HTML5</title>
</head>
<body>
Server is online
</body>
</html>
이 두 파일을 활용한 Dockerfile 은 다음과 같습니다.
$ vi Dockerfile
FROM alpine:3.8
RUN apk update
RUN apk add --no-cache nginx
RUN adduser -D -g 'www' www
RUN mkdir /www
RUN chown -R www:www /var/lib/nginx
RUN chown -R www:www /www
COPY nginx.conf /etc/nginx/nginx.conf
COPY index.html /www/index.html
ENTRYPOINT ["nginx"]
CMD ["-g", "daemon off;"]
docker 이미지를 빌드하고 실행시키면 index.html 결과를 얻을 수 있습니다.
$ sudo docker build -t seungkyua/nginx-alpine .
$ docker run -d -p 30080:80 --name nginx-alpine seungkyua/nginx-alpine
$ curl http://localhost:30080
하나의 RUN 명령어로 구성
여기서 첫번째 이미지 최적화 포인트가 보입니다.
앞의 Dockerfile 에서 하나의 RUN 은 하나의 이미지 레이어가 되므로 이것을 하나로 다음과 같이 줄일 수 있습니다.
RUN apk update && \
apk add --no-cache nginx && \
adduser -D -g 'www' www && \
mkdir /www && \
chown -R www:www /var/lib/nginx && \
chown -R www:www /www
이번에는 nodejs docker 이미지를 만들어 보겠습니다.
$ package.json
{
"name": "docker_web_app",
"version": "1.0.0",
"description": "Node.js on Docker",
"private": true,
"author": "Seungkyu Ahn <seungkyua@gmail.com>",
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^4.16.1"
}
}
$ vi server.js
'use strict';
const express = require('express');
const PORT = 8080;
const HOST = '0.0.0.0';
const app = express();
app.get('/', (req, res) => {
res.send('Hello world\n');
});
app.listen(PORT, HOST);
console.log(`Running on http://${HOST}:${PORT}`);
$ vi Dockerfile
FROM node:8
RUN mkdir -p /app
COPY package*.json /app/
WORKDIR /app
COPY . /app
RUN npm install --only=production
EXPOSE 8080
CMD [ "npm", "start" ]
파일 COPY 와 관련 라이브러리 설치 순서가 중요
위의 Dockerfile 의 경우 현재 디렉토리 소스를 COPY 한 후에 npm install 을 수행합니다.
docker 이미지는 변경된 레이어만 build 되지만 연관된 하위 레이어까지 build 됩니다.
여기서는 현재 디렉토리 소스가 변경되면 npm install 을 매번 다시 수행합니다.
그러므로 일단 package 설치를 먼저하고 COPY 를 나중에 하면 package 설치 내용이 변경되지 않는다면 npm install 은 캐시를 바로 사용하여 설치하지 않습니다.
RUN npm install --only=production
COPY . /app
마지막으로, 컴파일 하는 소스의 docker 이미지를 살펴보겠습니다.
$ vi hello.c
#include <stdio.h>
int main () {
printf ("Hello, world!\n");
return 0;
}
$ vi Dockerfile
FROM alpine:3.8
RUN apk update && \
apk add --update alpine-sdk
RUN mkdir -p /app
COPY . /tmp
WORKDIR /tmp
RUN gcc hello.c -o hello
ENTRYPOINT ["/tmp/hello"]
Multistep build 활용
위의 경우에 c 컴파일을 하기 위해 c 컴파일로가 들어있는 sdk 패키지를 설치하고 바이너리 파일로 컴파일을 하므로 이미지 사이즈가 커집니다.
여기서 build 단계를 활용하면 sdk 패키지는 제외하고 최종 바이너리 파일만 docker 이미지에 넣을 수 있습니다.
$ vi Dockerfile
FROM alpine:3.8 AS build
RUN apk update && \
apk add --update alpine-sdk
RUN mkdir -p /app
COPY . /tmp
WORKDIR /tmp
RUN gcc hello.c -o hello
FROM alpine:3.8
COPY --from=build /tmp/hello /app/hello
ENTRYPOINT ["/app/hello"]
아래 이미지 사이즈는 build 스텝을 활용하지 않은 파일 사이즈와 활용한 사이즈의 차이입니다.
seungkyua/c-hello-world 176MB
seungkyua/c-hello-world 4.42MB