앞선 글에서 Express를 이용해 클러스터를 구성해 여러 워커를 가진 서버를 띄워보았습니다. 이번에는 제작한 웹 서버를 이용해 이중화 구성을 한 뒤 nginx를 이용해 로드밸런서를 만들어 그럴듯한 운영 환경을 구성해 보도록 하겠습니다.

 

 

 

0. 사전 준비.

 

이 글에서는 앞선 글에서 작성한 웹 서버를 사용합니다. 이전 글을 참고해 웹 서버를 준비해 주세요.

Nginx를 사용할 예정이니 미리 서버를 다운로드하여 주셔도 좋습니다. 단, Docker를 이용하실 분은 별도의 Nginx를 준비하지 않으셔도 됩니다.

모든 결과물을 한번에 배포하기 위해 docker-compose를 사용할 예정입니다. docker-compose를 준비해 주세요.

 

 

 

1. Nginx를 이용한 이중화 구성.

 

이제 우리가 만든 서버는 여러 개의 워커를 갖고 도커 위에서 동작합니다. 이제 안정성을 위해 두 개의 서버를 올려두고 그 앞에 Nginx를 둬서 로드밸런서의 역할을 하도록 합시다.

 

우리가 할 일은 단지 nginx의 config파일을 추가하는것 뿐 입니다. 아무런 추가 작업을 진행하지 않은 채 설정 파일을 추가해 주는 것만으로도 로드밸런서를 구성할 수 있습니다.

 

새로운 폴더인 my-nginx를 생성하고 그 안에 nginx 폴더를 만들어줍니다. 그리고 그 안에 nginx.conf 파일을 작성합니다.

 

#./nginx/nginx.conf

user  nginx;
worker_processes  1;

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;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile        on;
    #tcp_nopush     on;

    keepalive_timeout  65;

    #gzip  on;

    include /etc/nginx/conf.d/*.conf;
}

 

사실 위의 설정파일은 nginx를 설치하면 가장 기본으로 들어가 있는 값입니다. 스킵하셔도 무방합니다만 나중에 수정된 값을 사용할 수 있으므로 넣어주었습니다.

 

설정 파일을 살펴봅시다. nginx.conf의 맨 아래 include를 보세요. 이 include가 의미하는 바는 /etc/nginx/conf.d폴더 내의 모든 conf파일을 가져와 http 아래에 두겠다는 뜻입니다.

 

따라서 우리가 다음에 작성할 설정 파일이 바로 아래 추가될 것이란 의미가 됩니다. 이 외에도 nginx의 default 페이지가 담긴 default.conf도 추가될 것입니다. 

 

 

그다음으로nginx 안에 conf.d 폴더를 생성 한 뒤 my-react-lb.conf파일을 작성합니다.

 

# ./nginx/conf.d/my-react-lb.conf

upstream my-react {
    #least_conn;
    #ip_hash;
    server localhost:3000 weight=10 max_fails=3 fail_timeout=10s;
    server localhost:3001 weight=10 max_fails=3 fail_timeout=10s;
}    
server {
    listen                8080;
    server_name  localhost;
    location / {
        proxy_pass http://my-react;
    }
}

 

이 파일의 폴더 경로도 실제 리눅스 시스템에서 nginx가 위치하는 폴더의 경로를 맞추기 위함이므로 임의의 폴더에 작성하셔도 됩니다.

 

server항목을 먼저 봅시다. 이 서버는 8080 포트에서 동작하고 리버스 프록시로 동직 합니다. 즉, localhost:8080으로 들어오면 내부적으로 my-react로 연결시킨다는 뜻입니다. 이때 연결하는 my-react가 위에서 정의한 upstream입니다.

 

upstream 내부에 두 개의 서버가 정의되어 있습니다. 유저가 들어올 때마다 번갈아 가면서 3000, 3001 포트에 올라간 서버가 접속을 수용할 겁니다. 정리하자면 localhost:8080으로 접속하게 되면 내부적으로 로드밸런싱을 거쳐 localhost:3000, locahost:3001로 이동하게 된다는 겁니다.

 

주석 처리한 least_conn은 번갈아 가면서 수용하는 게 아닌 현재 접속이 가장 적은 곳에 우선 접속할 수 있도록 하는 기능이며 ip_hash는 접속한 사용자의 ip를 해싱해서 같은 사용자는 같은 서버로 접속할 수 있게 해 세션 문제 등을 해결할 수 있도록 하는 기능입니다.

 

따라서 메인 서버에 위의 설정 파일을 갖는 nginx를 설치한 뒤 Docker로 3000, 3001 포트에 앞서 작성한 웹 서버를 두대 올려 둔 후 localhost:8080으로 접속하면 정상적으로 로드밸런싱이 동작할 겁니다. 

 

 

 

2. Docker compose를 이용해 한번에 배포하기.

 

 

사실 위와 같이 배포하기 위해서는 서버에 nginx를 직접 설치해야 하며 그 서버에서 Docker가 동작 해 웹서버가 돌아야 합니다. 즉, nginx가 돌아가는 localhost에 웹서버가 동작해야 한다는 뜻입니다.

 

서버에 접속해 nginx를 설치하고 conf 파일을 수정해준 뒤 그 서버에 docker를 설치하고 docker 명령어로 두 개의 웹서버를 실행시키는 과정이 필요하게 됩니다. 이것만 해도 귀찮아지기 시작합니다. 게다가 수작업이 들어가는 만큼 오류의 가능성도 높아집니다.

 

이제 좀 더 편하게 위 과정을 다음과 같이 변경할 겁니다.

  1. Nginx를 직접 설치하지 않고 Docker Image로 빌드 해 Docker 명령어로 배포한다
  2. 명령어를 미리 작성해 둬 한번에 여러 이미지를 배포한다.

그러기 위해선 docker-compose와 nginx를 docker image로 빌드하는 작업이 필요합니다. 우선 nginx를 docker image로 빌드하기 위해 Dockerfile을 작성합니다.

 

# ./Dockerfile

FROM nginx:stable

COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./nginx/conf.d/my-react-lb.conf /etc/nginx/conf.d/my-react-lb.conf

CMD ["nginx", "-g", "daemon off;"]

 

매우 간단합니다. nginx 안정화 버전을 받아 우리가 작성한 conf파일을 복사 한 뒤 실행합니다. 이제 다음 명령어로 docker image를 빌드합니다.

 

$ docker build . -t my-nginx:0.0.1

 

그리고 빌드한 이미지를 실행해 봅시다. 

 

$ docker run -itd -p 3000:3000 my-react:0.0.1

$ docker run -itd -p 3001:3000 my-react:0.0.1

$ docker run -itd -p 8080:8080 my-nginx:0.0.1

 

localhost:8080으로 이동하면 어떻게 될까요?

 

 

에러가 보이는 이유에 대해 생각해 봅시다. 우리는 nginx를 도커로 돌렸으며 my-react-lb.conf의 upstream에 server로 localhost:3000와 localhost:3001을 등록했습니다. 과연 nginx 컨테이너 내에서 이 두 서버에 접속할 수 있을까요?

 

궁금하신 분은 다음 명령어로 한번 테스트해보시기 바랍니다.

 

$ docker exec -u 0 -it (my-nginx 컨테이너 ID) bash

$ apt-get update

$ apt-get intall telnet

$ telnet localhost 3000

 

당연히 독립된 네트워크 이므로 접속을 할 수 없습니다.

 

 

그러므로 우리는 Docker compose를 이용해야 합니다. Docker compose는 같이 배포되는 컨테이너끼리 미리 정의된 이름으로 접속이 가능합니다. 마치 도메인 네임처럼 말입니다.

 

최상위 폴더로 이동해 docker-compose.yml 파일을 생성합니다 저 같은 경우엔 my-react와 my-nginx를 포함하고 있는 폴더에 작성하였습니다.

 

 

그리고 docker-compose.yml을 다음과 같이 작성해 줍니다.

 

# docker-compose.yml

version: "3"
services:
    my-react-A:
        image: my-react:0.0.1
        ports:
            - "3000:3000"
    my-react-B:
        image: my-react:0.0.1
        ports:
            - "3001:3000"
    nginx:
        image: my-nginx:0.0.1
        ports:
            - "8080:8080"

 

여기서 서비스 항목을 봅시다. 우리는 my-react-A, my-react-B, nginx 이렇게 총 세 개의 컨테이너를 생성하도록 작성하였습니다. 당연히 이름은 변경해도 됩니다. 서비스 내의 image는 도커 이미지를 의미합니다. ports는 docker run의 -p 옵션과 동일합니다. 

 

이런 식으로 docker-compose를 구성하게 되면 위의 세 컨테이너 간에는 서비스에 작성한 이름으로 서로 접근이 가능해집니다. 따라서 우리의 my-react-lb.conf가 바뀌어야 함을 의미하죠.

 

# ./nginx/conf.d/my-react-lb.conf

upstream my-react {
    #least_conn;
    #ip_hash;
    server my-react-A:3000 weight=10 max_fails=3 fail_timeout=10s;
    server my-react-B:3000 weight=10 max_fails=3 fail_timeout=10s;
}    
server {
    listen                8080;
    server_name  localhost;
    location / {
        proxy_pass http://my-react;
    }
}

 

그리고 다시 도커 이미지를 빌드해 줍니다.

 

$ docker build ./my-nginx -t my-nginx:0.0.2

 

잊지 마세요. 태그에 버전을 0.0.2로 변경하였으므로 당연히 docker-compose를 변경해야 합니다. 이제 docker-compose를 통해 컨테이너를 올려보겠습니다.

 

$ docker-compoase up -d

 

우리가 작성한 docker-compomse파일 대로 컨테이너가 실행되었습니다. 이제 localhost:8080으로 이동해 보세요. react페이지가 보이시나요? 

 

그렇다면 우리가 어떤 서버로 접속했는지 알아보기 위해 localhost:8080/where로 이동해 봅시다.

 

 

위와 같이 접속할 때마다 서버 정보가 변경되는 것을 확인할 수 있습니다. 실제 운영환경에선 이렇게 하면 서버가 바뀌며 세션정보가 유실되므로 앞서 설명한 ip_hash와 least_conn옵션을 켜고 서버를 배포해야 합니다.

 

 

 

 

 

 

 

서버 운영에 있어서 성능과 안정성은 매우 중요한 요소들 중 하나입니다. 이 글에선 Express 서버를 이용할 때 여러 워커를 클러스터 구성을 이용해 서비스하고 NGINX를 이용해 이중화 구성을 하는 방법에 대해 알아보도록 합니다.

 

 

 

0. 사전 준비.

 

이 글에서 웹서버는 React와 Express를 사용해 배포한 서버를 사용할 예정입니다. 실제 배포는  Docker를 사용해 도커 이미지를 생성한 후 실행시키도록 할 예정입니다. 미리 NPM와 Docker를 준비합시다.

마지막으로 모든 작업 결과물을 한번에 구동시키기 위해 Docker compose를 사용할 예정이니 Docker compose도 준비해 주세요.

 

 

 

1. NodeJS

 

 

NodeJS는 구글이 구글 크롬에 사용하려고 제작한 V8 오픈소스 자바스크립트 엔진을 기반으로 제작된 자바스크립트 런타임입니다. NodeJS는 다음과 같은 특징이 있습니다.

  1. 단일 스레드.
  2. 비동기 방식.
  3. 이벤트 루프를 사용.
  4. NPM.

NodeJS는 싱글 스레드이기 때문에 하나의 CPU를 여럿이 나눠 갖는 건 비효율적입니다. 따라서 CPU 숫자에 맞춰서 서버를 띄워보겠습니다.

 

 

 

2. 웹서버 생성.

 

 

우선 프론트를 준비합니다. 다음 명령어로 react app을 생성해 주세요.

 

$ npx create-reac-app my-react

 

이 앱을 Express 서버로 서비스할 예정입니다. 폴더로 들어가서 다음 패키지를 설치하세요.

 

$ cd my-react

 

$ npm i express express-favicon 

 

이제 express 서버를 실행할 코드를 작성해야 합니다. server 폴더에 index.js파일을 만드신 후 다음과 같이 코딩해 주세요.

 

//// ./server/index.js

const express = require('express');
const favicon = require('express-favicon');
const path = require('path');

const app = express();
const port = 3000;
const server = app.listen(port, () => {
    console.log(`Server is listening on port ${server.address().port}.`);
});

app.use(favicon(path.join(__dirname, '../public/favicon.ico')));
app.use(express.static(__dirname));
app.use(express.static(path.join(__dirname, '../build')));

app.get('/ping', (req, res) => {
    return res.send('pong');
});
app.get('/*', (req, res) => {
    return res.sendFile(path.join(__dirname, '../build', 'index.html'));
});

 

이제 기본으로 생성된 react를 빌드해 줍시다.

 

$ npm run build

 

여기까지 수행하셨으면 폴더 구조가 다음과 같을 겁니다.

 

 

우리가 작성한 express서버가 react app을 잘 서빙하는지 확인해 봅시다. localhost:3000으로 이동해 react 앱을 확인해 보세요. 그리고 localhost:3000/ping으로 이동해 pong 메시지를 잘 수신하는지도 확인합시다.

 

 

 

3. 클러스터 구성.

 

앞서 설명한 대로 NodeJS는 단일 스레드입니다. 이제 우린 서버의 CPU개수에 맞춰서 여러 워커를 띄어서 서버를 운영해 보도록 하겠습니다.

 

여러 워커가 돌아갈 때 워커가 동작하는 서버를 구분하기 위해 uuid 패키지를 설치합니다.

 

$ npm i uuid

 

그리고./server/index.js를 다음과 같이 수정해주세요.

 

//// ./server/index.js
const cluster = require('cluster');
const os = require('os');
const uuid = require('uuid');

const port = 3000;
const instance_id = uuid.v4();

//// Create worker.
const cpu_count = os.cpus().length;
const worker_count = cpu_count / 2;

//// If master, create workers and revive dead worker.
if(cluster.isMaster){
    console.log(`Server ID: ${instance_id}`);
    console.log(`Number of server's CPU: ${cpu_count}`);
    console.log(`Number of workers to create: ${worker_count}`);
    console.log(`Now create total ${worker_count} workers ...`);

    //// Message listener
    const workerMsgListener = (msg) => {
        const worker_id = msg.worker_id;
        //// Send master's id.
        if(msg.cmd === 'MASTER_ID'){
            cluster.workers[worker_id].send({cmd: 'MASTER_ID', master_id: instance_id});
        }
    }

    //// Create workers
    for(var i = 0; i < worker_count; i ++){
        const worker = cluster.fork();
        console.log(`Worker is created. [${i +1}/${worker_count}]`);
        worker.on('message', workerMsgListener);
    }

    //// Worker is now online.
    cluster.on('online', (worker) => { console.log(`Worker is now online: ${worker.process.pid}`); });

    //// Re-create dead worker.
    cluster.on('exit', (deadWorker) => {
        console.log(`Worker is dead: ${deadWorker.process.pid}`);
        const worker = cluster.fork();
        console.log(`New worker is created.`);
        worker.on('message', workerMsgListener);
    });
}
//// If worker, run servers.
else if(cluster.isWorker){
    const express = require('express');
    const favicon = require('express-favicon');
    const path = require('path');

    const app = express();
    const worker_id = cluster.worker.id;
    const server = app.listen(port, () => { console.log(`Server is listening on port ${server.address().port}.`); });

    let master_id = "";

    //// Request master's id to master.
    process.send({worker_id: worker_id, cmd: 'MASTER_ID'});
    process.on('message', (msg) => {
        if(msg.cmd === 'MASTER_ID'){
            master_id = msg.master_id;
        }
    });

    app.use(favicon(path.join(__dirname, '../public/favicon.ico')));
    app.use(express.static(__dirname));
    app.use(express.static(path.join(__dirname, '../build')));

    app.get('/ping', (req, res) => { return res.send('pong'); });
    app.get('/where', (req, res) => { return res.send(`Running server: ${master_id} \n Running worker: ${worker_id}`); });
    app.get('/kill', (req, res) => { cluster.worker.kill(); return res.send(`Called worker killer.`);})
    app.get('/*', (req, res) => { return res.sendFile(path.join(__dirname, '../build', 'index.html')); });
}

 

소스가 길어졌으니 주석을 확인하면서 차근차근 코딩해 주세요. 간단히 설명해드리자면 다음과 같습니다.

  1. 우리는 한 서버의 CPU개수의 절반만큼의 워커를 생성할 겁니다.
  2. 만약 현재 생성된 클러스터가 마스터라면 워커 클러스터를 생성하고 관리하는 역할을 수행합니다.
  3. 마스터는 미리 정해진 개수만큼 워커를 생성하고 만약 죽은 워커가 발견된 경우 새 워커를 생성시켜 항상 일정 개수의 워커가 서버에서 동작할 수 있도록 합니다.
  4. 워커는 express 서버를 구동합니다.
  5. 워커에는 어느 서버에서 수행되고 있는지 확인할 수 있는 기능과 현재 워커를 죽일 수 있는 기능이 추가되었습니다.

코딩이 끝났다면 다시 서버를 실행시켜 보도록 합시다.

 

$ node server

 

콘솔에서 다음과 같은 메시지를 확인할 수 있습니다.

 

 

이제 localhost:3000/where로 이동해 보세요. 현재 우리가 접속한 서버와 워커의 번호를 확인할 수 있습니다. 여기서 localhost:3000/kill로 이동하면 워커를 죽일 수 있으며 이때 마스터는 죽은 워커를 확인 해 새 워커를 생성합니다. 

 

 

이후 다시 localhost:3000/where로 이동하면 워커의 번호가 변경된 것을 확인할 수 있습니다.

 

 

 

4. Docker를 이용한 서버 구동

 

 

이제 우리가 작성한 서버를 도커를 이용해 배포해봅시다. 간단히 배포하기 위해 Dockerfile을 사용해 배포할 예정입니다. 

 

Dockerfile 파일을 생성 한 뒤 다음과 같이 작성해 주세요.

 

# ./Dockerfile

FROM node:slim

# app 폴더 생성.
RUN mkdir -p /app

# 작업 폴더를 app폴더로 지정.
WORKDIR /app

# dockerfile과 같은 경로의 파일들을 app폴더로 복사
ADD ./ /app

# 패키지 파일 설치.
RUN npm install

# 환경을 배포 환경으로 변경.
ENV NODE_ENV=production

#빌드 수행
RUN npm run build

ENV HOST=0.0.0.0 PORT=3000
EXPOSE ${PORT}

#서버 실행
CMD ["node", "server"]

 

아주 간단한 Dockerfile을 작성했습니다. 주석을 참고하시면 모두 이해하실 수 있을 겁니다. 이제 다음 명령어를 통해 작성한 Dockerfile을 이용해 도커 이미지를 생성합니다.

 

$ docker build . -t my-react:0.0.1

 

 

도커 이미지가 정상적으로 생성된 것을 확인할 수 있습니다. 다음 명령어를 통해 직접 실행시켜 주세요.

 

$ docker run -itd -p 8080:3000 my-react:0.0.1

 

이제 localhost:8080로 이동하면 모든 기능을 정상적으로 사용할 수 있습니다.

 

 

 

 

 

 

이전에 설치한 Harbor에 HTTPS접속을 하는 방법에 대해 알아봅니다.

 

 

 

0. Harbor설치

 

HTTPS를 제외한 Harbor설치는 이 글을 참고해 주시기 바랍니다.




1. certbot 설치.

 

인증서를 Let's Encrypt에서 발급 받아볼 예정입니다. 공식 홈페이지에서도 나와있지만 셸에 접근이 가능하다면 certbot을 통해 발급받는 것을 추천하고 있습니다.

 

다음 명령어를 통해 certbot을 사용하기 위한 준비를 합니다.

 

> sudo apt-get update

> sudo apt-get install software-properties-common

> sudo add-apt-repository universe

> sudo add-apt-repository ppa:certbot/certbot

**** 만약 우분투 서버 버전이 20.04라면 "sudo apt-add-repository -r ppa:certbot/certbot"를 실행하세요

> sudo apt-get update

 

위의 명령어를 모두 실행 한 뒤 다음 명령어를 통해 certbot을 설치합니다.

 

> sudo apt-get install certbot python-certbot-nginx

 

 

 

2. 인증서 발급.

 

만약 harbor가 실행중이라면 nginx에러가 발생합니다. 먼저 다음 명령어를 통해 harbor를 내려줍시다.

 

> sudo docker-compose down -v

 

 

그리고 certbot을 이용해 인증서를 발급받습니다.

 

> sudo certbot certonly --standalone 

 

 

 

 

3. 인증서 적용.

 

인증서 파일과 키 파일이 생성되었습니다. 다루기 편하게 두 파일을 cert 폴더로 복사합니다.

 

> mkdir cert

> cd cert

> sudo cp /etc/letsencrypt/libe/[DOMAIN]/fullchain.pem fullchain.pem

> sudo cp /etc/letsencrypt/libe/[DOMAIN]/privkey.pem privkey.pem

 

공식 홈페이지에선 OpenSSL을 이용한 방식의. crt파일과. key파일을 넣어주고 있지만 pem파일을 그대로 넣어도 잘 동작합니다.

 

이걸 그대로 설정 파일에 사용하도록 변경해 봅시다.

 

 

이제 docker-compose파일을 다시 생성해야 합니다. 아래의 명령어로 docker-compose파일을 다시 생성해 주세요.

 

> subo ./prepare

 

다시 생성된 docker-compose파일을 적용하기 위해 harbor를 실행해줍니다.

 

> sudo docker-compose up -d

 

 

** 만약 harbor가 이미 실행 중이었다면 먼저 종료한 후 다시 키셔야 합니다.

 

이제 입력한 도메인/ip주소로 이동해 보시면 인증서가 적용된 것을 확인할 수 있습니다.

 

 

 

 

 

 

 

Harbor를 이용해 Docker Private Registry를 구축해 봅니다.

 

 

 

1. Harbor

 

Harbor는 비교적 덜 알려진 도커 레지스트리들 중 하나입니다. 공식 문서에 따르면 Harbor는 다음과 같이 설명하고 있습니다.

 

Harbor는 컨텐츠를 저장, 서명 및 스캔하는 오픈 소스 신뢰할 수 있는 클라우드 기본 레지스트리 프로젝트입니다. Harbor는 보안, ID 및 관리와 같은 사용자가 일반적으로 필요로 하는 기능을 추가하여 오픈 소스 Docker Distribution을 확장합니다. 

빌드 및 실행 환경에 더 가까운 레지스트리를 사용하면 이미지 전송 효율성을 향상시킬 수 있습니다. Harbor는 레지스트리 간 이미지 복제를 지원하고 사용자 관리, 액세스 제어 및 활동 감사와 같은 고급 보안 기능을 제공합니다.

Harbor는 CNCF (Cloud Native Computing Foundation)에서 호스팅 합니다. 클라우드 네이티브 기술의 진화를 구체화하려는 조직인 경우 CNCF 가입을 고려하십시오. 참여자 및 Harbor의 역할에 대한 자세한 내용은 CNCF 발표를 읽으십시오.

 

 

또한 Harbor에서 제공하는 주요한 기능은 다음과 같습니다:

 

  • 클라우드 네이티브 레지스트리: 컨테이너 이미지와 Helm 차트를 모두 지원하는 Harbor는 컨테이너 런타임 및 오케스트레이션 플랫폼과 같은 클라우드 네이티브 환경의 레지스트리 역할을 합니다.
  • 역할 기반 액세스 제어: 사용자 및 리포지토리는 '프로젝트'를 통해 구성되며 사용자는 프로젝트에서 이미지 또는 Helm 차트에 대해 서로 다른 권한을 가질 수 있습니다.
  • 정책 기반 복제: 여러 필터(리포지토리, 태그 및 레이블)가 있는 정책을 기반으로 여러 레지스트리 인스턴스 간에 이미지 및 차트를 복제(동기화) 할 수 있습니다. Harbor는 오류가 발생하면 자동으로 복제를 재 시도합니다. 로드 밸런싱, 고 가용성, 다중 데이터 센터, 하이브리드 및 다중 클라우드 시나리오에 적합합니다.
  • 취약점 검색: Harbor는 이미지를 정기적으로 스캔하여 사용자에게 취약점을 경고합니다.
  • LDAP/AD 지원: Harbor는 사용자 인증 및 관리를 위해 기존 엔터프라이즈 LDAP / AD와 통합되며 LDAP 그룹을 Harbor로 가져오고 적절한 프로젝트 역할을 할당할 수 있습니다.
  • OIDC 지원 : Harbor는 OIDC(OpenID Connect)를 활용하여 외부 인증 서버 또는 자격 증명 공급자가 인증 한 사용자의 자격 증명을 확인합니다. Single sign-on을 사용하여 Harbor 포털에 로그인할 수 있습니다.
  • 이미지 삭제 및 가비지 수집: 이미지를 삭제하고 공간을 재활용할 수 있습니다.
  • 공증(Notary): 이미지 진위를 보장할 수 있습니다.
  • 그래픽 사용자 포털: 사용자는 쉽게 저장소를 탐색하고 검색하며 프로젝트를 관리할 수 ​​있습니다.
  • 감사: 리포지토리에 대한 모든 작업이 추적됩니다.
  • RESTful API: 대부분의 관리 작업을 위한 RESTful API로 외부 시스템과 쉽게 통합할 수 있습니다. API를 탐색하고 테스트하기 위해 임베디드 Swagger UI를 사용할 수 있습니다.
  • 쉬운 배포: 온라인 및 오프라인 설치 관리자를 모두 제공합니다. 또한 Helm Chart를 사용하여 Kubernetes에 Harbor를 배포할 수 있습니다.

만약 Harbor대신 다른 레지스트리들의 옵션을 알고 싶다면이 글을 참고해 주시기 바랍니다.

 

 

 

2. Harbor 설치 준비 - 요구 사양

 

Harbor는 리눅스에서 동작합니다. 이 글에서는 리눅스 서버 18.04.3 버전을 사용합니다.

 

하드웨어 요구사항은 다음과 같습니다.

리소스 최소 사양 권장 사양
CPU 2 CPU 4CPU
메모리 4 GB 8 GB
스토리지 40 GB 160 GB

 

소프트웨어 요구사항은 다음과 같습니다.

소프트웨어 버전
Docker Engine 17.06.0 CE 버전 이상
Docker Compose 1.18.0 버전 이상
OpenSSL 최신 버전 권장

 

도커 설치는 이 글을, 도커 컴포즈 설치는 이 글을 참고해주세요.

 

 

네트워크 포트 사용은 다음과 같습니다.

 

포트 프로토콜 설명      
443 HTTPS Harbor 포털 및 코어 API는이 포트에서 HTTPS 요청을 수락합니다. 구성 파일에서이 포트를 변경할 수 있습니다.
4443 HTTPS 하버 용 Docker Content Trust 서비스에 대한 연결입니다. 공증(Notary)이 활성화 된 경우에만 필요합니다. 구성 파일에서이 포트를 변경할 수 있습니다.
80 HTTP Harbor 포털 및 코어 API는이 포트에서 HTTP 요청을 수락합니다. 구성 파일에서이 포트를 변경할 수 있습니다.

 

 

 

3. Harbor 설치

 

우선 Harbor 릴리즈 페이지로 이동해 Harbor 파일을 다운로드합니다. 이 글에선 현재(20.02.14) 최신 버전인 1.10.1 버전의 오프라인 버전을 다운로드하여서 설치해 보도록 하겠습니다.

 

.asc 파일은 옵션입니다. 이 파일은 OpenPGP 키 파일로 다운로드한 번들이 정품인지 확인하는 데 사용됩니다. 다운로드한 파일을 검증하는 방법은 여기서 확인해 주시기 바랍니다.

 

다운로드한 파일이 있는 경로로 이동해서 다음 명령어를 통해 파일을 추출합니다.

 

$ tar xvf harbor-offline-installer-v1.10.1.tgz

 

./harbor/harbor.yml파일을 수정하여 설정을 변경할 수 있습니다. 설치를 위해 호스트 이름을 변경합시다.

 

$ vi harbor.yml

호스트 설정은 파일 가장 위에 있습니다.

localhost나 127.0.0.1을 사용하지 말라고 되어있네요. 적당한 호스트를 입력한 후 저장합니다. 

이 설정 파일엔 초기 접속 정보 및 DB정보도 기재되어있습니다. 우선 관리자 암호를 기억해 둡시다.

실제 사용을 원한다면 당연히 HTTPS를 사용해야 합니다만 우선 간단한 설치를 위해 HTTPS 사용을 비활성화합니다.

 

설정을 저장한 후 다음 명령어로 install.sh을 실행시킵니다.

 

$ sudo ./install.sh

 

위와 같이 설치가 정상적으로 끝나면 앞서 입력한 호스트네임으로 접속해 봅시다. 다음과 같이 Harbor admin페이지를 확인할 수 있습니다.

기본 계정과 암호로 로그인합니다. 로그인 정보는 admin/Harbor12345입니다. 만약 설정 파일에서 초기 비밀번호를 바꾸셨다면 그 암호를 입력해 로그인합니다.

 

정상적으로 로그인되는 것을 확인할 수 있습니다.

 

 

 

 

 

이 글은 "Container Registries You Might Have Missed"을 번역한 글 입니다.

 

 

 

 

1. Intro

 

레지스트리는 주로 Docker와 같은 컨테이너 작업을 수행하는데에 있어서 주요 구성 요소 중 하나이므로 대중들에게 매력적입니다. 

 

레지스트리는 컨테이너 엔진의 호스트에서 다운로드되고 실행되는 이미지를 호스팅합니다. 컨테이너는 단순히 특정 이미지가 실행중인 인스턴스입니다. 이미지는 Microsoft Windows의 MSI 또는 Red Hat Enterprise Linux의 RPM과 같이 바로 사용할 수있는 패키지로 생각하면 됩니다.

 

여기서는 레지스트리가 어떻게 작동하는지 자세히 설명하지 않겠지만 더 자세히 알고 싶다면 이 기사를 읽어 보세요. 그 대신 제가 이 글에서 하고 싶은 것은 여전히 레이더 아래에 남아있는 몇몇의 컨테이너 레지스트리를 강조하는것 입니다. 

 

널리 알려진 이름의 레지스트리는 Docker를 사용하는 대부분의 사람들에게 이미 익숙하지만 이미지를 호스팅 할 위치를 결정할 때 고려해야 할 가치가 있는 작은 레지스트리도 있습니다. 

 

잘 알려지지 않은 컨테이너 레지스트리에 대한 설명은 계속 읽으시면 됩니다.

 

 

 

2. 잘 알려진 레지스트리들

 

먼저 잘 알려진 레지스트리를 식별해 잘 알려지지 않은 레지스트리와 비교할 내용을 명확히 하겠습니다.

 

현재 모든 계정에서 가장 인기있는 레지스트리는 Docker Hub입니다. Docker Hub는 알려진 레지스트리 세계의 중심입니다. 이건 모든 Docker 설치가 참조하도록 구성된 기본 호스팅 된 레지스트리입니다. 

 

그 외의 다른 임기 있는 레지스트리는 다음과 같습니다:

 

 

 

3. 당신이 놓치고 있을지도 모르는 레지스트리들

 

이제 흥미로운 부분으로 넘어 갑니다. 덜 알려진 레지스트리의 개요는 다음과 같습니다.

  • Amazon EC2 Container Registry (ECR)
  • FlawCheck Private Registry
  • GitLab Container Registry
  • Portus by SUSE
  • Sonatype Nexus
  • VMware Harbor Registry

 

3.1. 아마존 EC2 컨테이너 레지스트리(ECR)

 


아마존이 Amazon EC2 Container Service (ECS)라는 호스트 된 컨테이너 서비스를 제공한다는 것을 이미 알고 있을겁니다. 그러나 아마존이 ECS를 완성하기 위해 제공하는 레지스트리는 덜 관심을받는 경향이 있습니다. 

 

Amazon EC2 Container Registry (ECR)라고 불리는 레지스트리이며 호스트 된 Docker 컨테이너 레지스트리입니다. ECR은 ECS와 통합됩니다. 

 

2015 년 12 월에 도입 된이 솔루션은 잘 알려진 대부분의 레지스트리보다 새로운 레지스트리 옵션으로인해  일부 사용자에게 익숙하지 않다고 설명합니다. 

 

ECS는 ECR과 호환되는 유일한 컨테이너 레지스트리가 아닙니다. ECS는 외부 레지스트리도 지원합니다. 그러나 ECR의 주요 이점은 완전히 호스트 되고 관리되는 레지스트리이므로 배포 및 관리가 간단하다는점 입니다. 또한 ECR은 다른 ECS 인프라만큼 확장 가능하므로 확장성이 매우 뛰어납니다. 

 

최고의 사용 사례는 다음과 같습니다: AWS 서비스를 많이 사용하거나 사용할 계획을 갖고 있으며 개인 이미지를 호스팅 할 장소를 찾고 있다면 ECR을 사용하는 것이 좋습니다. 대규모 레지스트리 배포가 있거나 시간이 지남에 따라 레지스트리가 크게 확장 될 것으로 예상되는 경우에도 좋은 선택입니다. 이 경우 사실상 제한없이 ECR을 확장할 수 있습니다.

 

 

 

3.2. FlawCheck Private Registry

 

 

FlawCheck Private Registry (보안 업체 Tenable이 FlawCheck의 나머지 비즈니스와 함께 최근에 인수 함)는 보안을 중점으로 한 레지스트리 옵션입니다.

 

이 레지스트리는 컨테이너 이미지에 대한 통합 취약성 검색 및 말웨어 탐지 기능을 제공합니다. 컨테이너 이미지에 악성 코드가 없어지게 하거나 레지스트리에 악성 이미지가 삽입되는 것을 막는 만능 도구는은 아니지만 FlawCheck의 스캔 기능은 위험을 줄이는데 도움이 될 수 있습니다.

 

최상의 사용 사례는 다음과 같습니다: 보안에 민감한 회사의 경우이 방법이 정말 좋습니다. 규제가 심한 산업에서는 이 레지스트리를 많이 채택 할 것으로 예상됩니다.

 

 

 

3.3. GitLab Container Registry

 


호스트 또는 온-프레미스 레지스트리로 실행할 수있는 GitLab Container Registry는 컨테이너 이미지 호스팅을위한 GitLab의 솔루션입니다. GitLab에 내장되어 있으며 나머지 GitLab의 도구들과 완벽하게 호환되므로 GitLab 딜리버르 파이프 라인에 직접 통합 될수도 있습니다. 

 

팀이 가능한 한 적은 움직이는 부품으로 완벽한 DevOps 워크 플로우를 탐색하고 있다면 이를 채택하는것이 좋습니다. 

 

최상의 사용 사례는 다음과 같습니다: 일부 개발자는 Docker 이미지를 소스 코드와 동일한 플랫폼에 저장하는 것이 편리하다는 것을 알게 될 것입니다. 소스 코드에 GitLab을 사용한다면 GitLab Container Registry가 유용 합니다. 그러나 GitLab Container Registry는 대부분의 다른 레지스트리에서 사용할 수없는 킬러 기능을 제공하지 않습니다.

 

 

 

3.4. SUSE의 Portus

 


Portus는 기술적으로 레지스트리는 아니지만 Docker Registry의 온-프레미스 배포를 위해 기본 UI를 대체하는 프런트 엔드를 제공합니다. 

 

Portus는 추가 액세스 제어 옵션을 제공하여 Docker Registry에 가치를 더하도록 설계되었습니다. 여기에는 팀 또는 레지스트리 사용자를 서로 다른 액세스 수준을 설정하여 구성하는 기능이 포함됩니다.(여러 가지면에서이 기능은 Unix 계열 시스템의 사용자 그룹과 유사합니다.) 

 

또한 Portus는 레지스트리 네임스페이스를 제공해 팀의 사용자 뿐 아니라 개인 사용자에게 다른 저장소에 대해 세부적으로 수행할 수 있는 수정 유형 구성을 만드는것을 가능하게 합니다. 

 

그리고 Portus는 레지스트리 설정 및 액세스 제어를 구성 할 수있는 사용자 친화적 인 웹 인터페이스를 제공합니다. CLI 구성 도구 인 portusctl도 사용 가능합니다. 

 

최상의 사용 사례는 다음과 같습니다: Docker Registry를 좋아하지만 추가 보안 제어가 필요하거나 세분화 된 액세스 제어를 사용해야하는 이유가있는 경우 Portus는 강력한 솔루션입니다.

 

 

 

3.5. Sonatype Nexus

 


호스트 및 온-프레미스 배포를 지원하는 Sonatype Nexus는 범용 리포지토리입니다. 이는 Docker 이미지 호스팅보다 훨씬 많은 것을 지원하지만 Docker 레지스트리로도 사용될 수 있습니다. 

 

Docker보다 훨씬 오래 되었으며 이전에 컨테이너 레지스트리를 사용하지 않았더라도 노련한 관리자에게는 익숙 할 것입니다. 

 

핵심 Nexus 플랫폼은 오픈 소스이지만 상용 옵션 또한 제공됩니다. 

 

최상의 사용 사례는 다음과 같습니다: 많은 회사에서 Nexus를 Maven의 저장소로 몇 년 동안 배포해 왔습니다. 플랫폼의 최신 릴리스로 업그레이드하기 만하면 조직에서 Docker 이미지 호스팅을 추가하여 새로운 제품에 대한 개발 또는 운영 직원을 교육하지 않고도 자체 Docker 레지스트리를 만들 수 있습니다. 또한 Docker 이미지와 함께 다른 유형의 제작물를 호스팅 할 수 있습니다.

 

 

 

3.6. VMware Harbor Registry

 

 

Docker 생태계에서 VMware를 주요 플레이어로 생각하지는 않지만 이 회사는 확실히 새로운 시도를 하고 있습니다. Harbor Registry는 Docker 이미지 호스팅에 대한 VMware의 결과입니다. 

 

이 레지스트리는 Docker Distribution을 기반으로 구축되었지만 보안 및 ID 관리 기능을 추가하였습니다. 또한 단일 호스트에서 여러 레지스트리를 지원합니다. 

 

최상의 사용 사례는 다음과 같습니다: Harbour는 보안 및 사용자 관리에 중점을두고 있기 때문에 이 옵션은 기업이 추구하는 중요한 레지스트리 기능을 제공하며 이 기능은 다른 모든 레지스트리에서 사용할 수 있는것은 아닙니다. 이는 기업에서 좋은 선택입니다. 

 

Harbor는 Docker 컨테이너로 실행되기 때문에 Docker 환경이있는 모든 서버에 쉽게 설치할 수 있으며 개발자에게 오프라인 설치 프로그램을 제공하기 때문에 보안 고려 사항 또는 기타 요인이있는 상황(공용 인터넷에 연결할 수 없는 경우을 의미합니다.)에서 유용 할 수 있습니다.

 

 

 

4. Conclusion

 

서로 다른 레지스트리 오퍼링 사이의 주요 변수에는 지원하는 배포 환경 유형 (호스트, 온 프레미스 또는 둘 다)이 포함됩니다. 액세스 제어 옵션을 얼마나 미세하게 조정했는지; 컨테이너 레지스트리에 대한 추가 보안 수준. 

 

물론 필요에 맞는 레지스트리를 선택하는 것은 이러한 기능이 우선 순위에 어떻게 부합하는지에 달려 있습니다. 그러나 선택의 폭이 넓기 때문에 특정 조직의 요구에 완벽한 균형을 제공하는 레지스트리를 찾는 것은 어렵지 않을겁니다.

 

 

 

 

 

 

 

 

 

우분투 서버 18-04.3 버전에 docker-compose를 설치해 봅니다.

 

 

 

1. docker-compose 설치하기

 

GitHub의 Compose 저장소 릴리스 페이지에서 Docker Compose 바이너리를 다운로드할 수 있습니다.

터미널에서 curl 다음 명령을 실행하여 바이너리를 다운로드해봅시다.

 

$sudo curl -L "https://github.com/docker/compose/releases/download/1.26.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

 

 

** 만약 다른 버전을 사용하고 싶으시다면 "1.26.0"을 원하는 버전으로 변경해서 다운 받으시면 됩니다.

** 만약 알파인(alpine) 버전을 다운 받았다면 py-pip, python-dev, libffi-dev, openssl-dev, gcc, libc-dev, make의 추가 패키지가 필요할 수 있습니다.

 

 

20.02.14 추가 내용

 

정확한 원인은 알 수 없으나 1.25.0 버전을 위와같이 설치하면 계속 CURL 에러가 발생합니다. 만약 반드시 1.25.0 버전이 필요한 경우가 아니라면 다음 명령어를 통해 설치하는것을 추천드립니다.

 

$sudo curl -L https://github.com/docker/compose/releases/download/1.25.4/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose 

 

 

 

2. 권한 부여

 

정상적으로 다운받았다면 다음 명령을 실행 해 바이너리에 권한을 부여합니다.

 

$sudo chmod +x /usr/local/bin/docker-compose

 

 

** 만약 명령이 실패한다면 docker-compose의 경로를 확인해 주세요.

 

 

 

3. 설치 확인

 

정상적으로 설치되었는지 다음 명령어로 확인해 봅니다.

 

$docker-compose --version

 

 

 

 

** 수정. 

2020.06.28 docker compose 버전을 1.2.5에서 1.2.6으로 변경.

 

 

시놀로지 NAS를 이용한 개인 저장소 구축.

 

시놀로지 NAS를 이용해 Private Registry를 구축해 봅니다.

 

 

1. Registry 설치.

 

시놀로지 도커를 이용해 레지스트리를 설치합니다.

 

볼륨 설정은 다음과 같이 추가해줍니다.

 

포트는 기본포트를 사용하지 않고 5234번 포트를 사용했습니다.

 

 

 

2. 도메인 및 역방향 프록시 설정

 

저는 https://hub.smoh.kr/ 주소를 사용해 보겠습니다. 미리 도메인을 준비하시거나 그냥 IP주소를 사용하셔도 무방합니다. 확인해보지는 않았으나 QuickConnect를 사용해도 될 거 같긴 합니다.

역방향 프록시는 제어판 > 응용 프로그램 > 응용 프로그램 포털에 있습니다.

 

위와 같이 역방향 프록시를 설정해 줍니다.

이후 https://hub.smoh.kr/v2/_catalog 에 접속해 봅시다. 레지스트리가 정상적이라면 "{"repositories":[]}"와 같은 메시지를 확인할 수 있습니다. 그런데 인증서가 유효하지 않다고 나오네요. 

 

이제 인증서를 등록해 줍시다. 인증서는 제어판 > 연결성 > 보안 > 인증서 탭에서 추가할 수 있습니다. 여기선 Let's encrypt에서 인증서를 발급받아 사용하겠습니다.

 

매우 간단하게 인증서를 발급 받을 수 있습니다. 이제 구성에 들어가 hub.smoh.kr의 인증서를 새로 발급한 인증서로 변경해 줍니다.

 

이후 다시 https://hub.smoh.kr/v2/_catalog 에 접속해 보면 인증서가 제대로 적용된 것을 확인할 수 있습니다.

 

 

3. HTTP연결 자동으로 리디렉트 하기

 

위의 역방향 프록시를 보면 아시겠지만 현재 http://hub.smoh.kr/에 대한 처리가 없습니다. HTTP연결을 자동으로 HTTPS로 리디렉트 해주는 기능은 없으므로 DSM의 nginx에서 처리하도록 변경해 줍니다.

 

$ cd /usr/local/etc/nginx/sites-enabled
$ touch redirect.conf

파일명은 중요하지 않습니다. 적당한 이름의 conf파일을 만든 후 다음과 같이 수정합니다.

 

server {
  listen 80;
  server_name abc.domain.com;
  location / {
    rewrite ^(.*)$ https://abc.domain.com/$1 permanent;
  }
}

 

이후 nginx 리로드 합니다.

$ sudo nginx -s reload

그리고 http://hub.smoh.kr/을 통해 접속하면 자동으로 HTTPS로 리디렉트 되는 것을 확인할 수 있습니다.

 

 

4. 이미지 업로드 해보기

 

이제 테스트 이미지를 push하고 pull 해보도록 하겠습니다. 다른 접속지에서 도커 이미지를 내려받습니다.

 

$ docker pull ubuntu
$ docker images

 

우분투 이미지를 하나 다운받았습니다. 사진에서 보이듯이 앞서 레지스트리를 설치한 서버와는 다른 서버입니다. 이제 태그를 생성합니다.

 

$ docker tag ubuntu hub.smoh.kr/ubuntu
$ docker images

 

이미지가 새로 생성된걸 확인한 후 push를 시도합니다.

 

$ docker push hub.smoh.kr/ubuntu

 

이제 https://hub.smoh.kr/v2/_catalog로 이동하면 "{"repositories":["ubuntu"]}"와 같이 업로드한 이미지가 추가된 것을 확인할 수 있습니다.

 

가장 처음 폴더를 추가했던것을 기억하시나요 ? 그 폴더로 한번 이동해 봅시다.

 

사진과 같이 /docker/registry/v2 폴더가 새로 생성되었고 그 안에 업로드한 이미지가 저장된 것을 확인할 수 있습니다.

 

 

5. 유저 인증 추가

 

업로드도 확인했지만 만약 주소가 노출된 경우 어떤 유저도 해당 레지스트리에 접근할 수 있는 문제가 있습니다. 이 문제를 해결해 봅시다.

우선 시놀로지 도커에서 생성한 레지스트리 컨테이너를 종료합니다.  그후 다음과 같이 볼륨과 환경을 수정해줍니다.

 

설정 변경 후 컨테이너를 실행시키면 auth폴더 안에 htpasswd파일이 생성된 것을 확인할 수 있습니다. 이제 유저를 추가시키도록 합니다.

 

$ cd /volume1/docker/myReg
$ sudo docker run --entrypoint htpasswd registry -Bbn testuser testpassword > auth/htpasswd

위의 명령어를 시행하면 "testuser"계정이 "testpassword"의 암호를 갖은채 생성됩니다. 계정을 생성했으니 이제 https://hub.smoh.kr/v2/_catalog 를 통해 이미지 리스트를 불러오려고 시도해봅시다.

 

아까는 볼 수 없던 로그인창이 뜹니다. 로그인을 하면 정상적으로 이미지 리스트를 확인할 수 있습니다.

 

 

6. 이미지 다운로드

 

이제 다른 서버에서 이미지를 다운로드 받아봅시다.

$ docker pull hub.smoh.kr/ubuntu

 

위와 같이 인증관련 에러를 볼 수 있습니다. 이제 로그인을 한 뒤 이미지를 다운로드 받아보겠습니다.

 

$ docker login hub.smoh.kr
$ docker pull hub.smoh.kr/ubuntu

 

로그인 이후에 정상적으로 이미지를 다운받을 수 있습니다. 로그인 이후 ~/.docker/config.json에 인증정보가 저장되어 로그아웃하기 전까지 로그인 정보가 유지됩니다.

 

 

 

 

우분투에서 도커 설치하기

 

우분투에서 도커CE버전을 설치해 봅니다.

우분투는 Ubuntu-server-18.04.2 버전을 사용했습니다.

 

 

 

쉘 스크립트를 이용한 도커 설치

 

2020.04.14. 추가.

 

마침 도커를 다시 설치할 일이 생겼습니다. 기존에 포스트를 보니 여러 복잡한 과정을 거쳐서 설치를 하도록 안내되어 있어 간단한 설치방법을 알려드립니다.

 

아래의 명령어를 실행시키면 바로 도커가 설치됩니다.

 

curl -fsSL https://get.docker.com/ | sudo sh

 

우분투 버전은 본 글 그대로인 18.04.2버전을 사용하였습니다.

 

 

 

도커 저장소 사용을 위한 패키지 설치

 

도커CE를 설치하기 전에 도커 저장소를 설정해야 합니다. 저장소 설정전에 필요한 패키지들을 설치합니다.

$sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common

위의 명령어를 통해 필요한 패키지들을 설치합니다.

 

 

도커 저장소 설정

 

도커의 공식 GPG키를 추가합니다.

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

 

정상적으로 처리되었는지 핑거프린트를 확인해 봅니다.

전체 핑거프린트는 다음과 같습니다: 9DC8 5822 9FC7 DD38 854A E2D8 8D81 803C 0EBF CD88

여기서 마지막 8자리를 이용해 핑거프린트를 확인합니다.

$sudo apt-key fingerprint 0EBFCD88

 

핑거프린트를 확인 후 도커 저장소를 설정합니다

$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

 

 

저장소 설정 후 apt-get update를 잊지 마세요.

 

도커 CE 설치

 

$ sudo apt-get install docker-ce docker-ce-cli containerd.io

위의 명령어를 통해 도커 CE를 설치합니다.

 

설치 완료후 버전을 확인해 봅니다.

$ docker --version

 

+ Recent posts