다음 중 하나 이상 경험한 사람
다른 컴퓨터에서 빌드 에러가 나는 경우
빌드툴이 발전하면서 많이 나아졌지만 그래도 다른 OS문제라던지 여전히 가끔씩 접하는 케이스이다.
해결법
Dockerfile
을 사용한다
DB를 연결해야 되는데 내 컴퓨터를 사용하고 싶지 않은 경우
- 웹개발을 할때 프로덕션과 동일한 환경으로 세팅해야 하지만 귀찮다
- 이미 내 데이터베이스는 사용중이거나 혹은 귀찮다
- 크롤링을 하면서 데이터베이스에 저장하고 싶은데 데이터베이스 설정하기가 귀찮다
해결법
docker-compose
로 해결한다.
배포 너무 귀찮다..
배포 환경에서 환경설정 등 여러가지로 설정을 잡아 주기 귀찮은 상황도 docker로 해결 할 수 있다.
해결법
docker stack deploy
를 사용한다
서비스 scalability도 쉽게 했으면 좋겠다..
해결법
docker service scale frontend=10
한줄이면 된다.
Docker로 개발하면 아래와 같은 프로세스로 개발을 하게 된다
Dockerfile
작성docker-compose.yml
작성- 개발
docker-compose up
- 배포
- 이미지화 및
registry
docker stack deploy
- 이미지화 및
http://labs.play-with-docker.com/
Dockerfile
은 docker image를 작성하는 간단한 스크립트이다Dockerfile
로 image가 만들어지면 container로 실행이 가능하게 된다.- Virtualbox로 치면
- image = iso파일
- container = 실제 실행되는 가상환경
- 물론 docker는 virtualization이 아니라 호스트의 커널을 사용해서 사실상 Native Application이다.
예시로 아래와 같은 Flask App이 있다.
from flask import Flask, render_template
import random
app = Flask(__name__)
# list of cat images
images = [
"http://ak-hdl.buzzfed.com/static/2013-10/enhanced/webdr05/15/9/anigif_enhanced-buzz-26388-1381844103-11.gif",
"http://ak-hdl.buzzfed.com/static/2013-10/enhanced/webdr01/15/9/anigif_enhanced-buzz-31540-1381844535-8.gif",
"http://ak-hdl.buzzfed.com/static/2013-10/enhanced/webdr05/15/9/anigif_enhanced-buzz-26390-1381844163-18.gif",
"http://ak-hdl.buzzfed.com/static/2013-10/enhanced/webdr06/15/10/anigif_enhanced-buzz-1376-1381846217-0.gif",
"http://ak-hdl.buzzfed.com/static/2013-10/enhanced/webdr03/15/9/anigif_enhanced-buzz-3391-1381844336-26.gif",
"http://ak-hdl.buzzfed.com/static/2013-10/enhanced/webdr06/15/10/anigif_enhanced-buzz-29111-1381845968-0.gif",
"http://ak-hdl.buzzfed.com/static/2013-10/enhanced/webdr03/15/9/anigif_enhanced-buzz-3409-1381844582-13.gif",
"http://ak-hdl.buzzfed.com/static/2013-10/enhanced/webdr02/15/9/anigif_enhanced-buzz-19667-1381844937-10.gif",
"http://ak-hdl.buzzfed.com/static/2013-10/enhanced/webdr05/15/9/anigif_enhanced-buzz-26358-1381845043-13.gif",
"http://ak-hdl.buzzfed.com/static/2013-10/enhanced/webdr06/15/9/anigif_enhanced-buzz-18774-1381844645-6.gif",
"http://ak-hdl.buzzfed.com/static/2013-10/enhanced/webdr06/15/9/anigif_enhanced-buzz-25158-1381844793-0.gif",
"http://ak-hdl.buzzfed.com/static/2013-10/enhanced/webdr03/15/10/anigif_enhanced-buzz-11980-1381846269-1.gif"
]
@app.route('/')
def index():
url = random.choice(images)
return render_template('index.html', url=url)
if __name__ == "__main__":
app.run(host="0.0.0.0")
프로젝트 구조는 다음과 같다.
. ├── app.py ├── requirements.txt └── templates └── index.html
위 프로젝트로 Dockerfile
을 작성하면 다음과 같다.
# FROM: 베이스 이미지를 정해준다.
FROM python:3-alpine
# requirements.txt 파일 복사
COPY requirements.txt /code/
RUN pip install --no-cache-dir -r /code/requirements.txt
# app.py와 index.html 복사
COPY app.py /code/
COPY templates/index.html /code/templates/
EXPOSE 5000
WORKDIR /code
CMD ["python", "app.py"]
외울 필요는 없고 필요할때마다 Dockerfile Reference 에서 찾아보면 된다
몇가지 포인트는
FROM
- 베이스 이미지를 정해준다. 새로 만드는 것보다 official image 위를 이용하는게 좋고 alpine은 리눅스 종류 중 하나인데 용량이 5mb에 불과해 대부분 모든 공식 이미지들이 alpine을 지원한다.
EXPOSE
- 포트 5000을 열겠다는 소리이지만 실제로 아무 일도 하지 않는다. 사용자에게 포트 5000번에서 이 어플리케이션이 돌아간다고 알려주는 용도이다. 그렇지 않으면 포트 몇번을 연결해줘야 되는지 헷갈리기 때문이다. 실제로는
ENV
혹은ARG
로 설정해준다.
이제 이걸 이미지로 빌드한다
docker build -t my_flask_app .
Sending build context to Docker daemon 7.68kB Step 1/8 : FROM python:3-alpine ---> d26cf7d4701d Step 2/8 : COPY requirements.txt /code/ ---> Using cache ---> 459e0df94893 Step 3/8 : RUN pip install --no-cache-dir -r /code/requirements.txt ---> Using cache ---> 5a618308e9b8 Step 4/8 : COPY app.py /code/ ---> Using cache ---> e35197a2ee91 Step 5/8 : COPY templates/index.html /code/templates/ ---> Using cache ---> 734c689d69e3 Step 6/8 : EXPOSE 5000 ---> Using cache ---> 0c4d7984da84 Step 7/8 : WORKDIR /code ---> Using cache ---> 89bfc8e45446 Step 8/8 : CMD python app.py ---> Using cache ---> 51fb98413e28 Successfully built 51fb98413e28 Successfully tagged my_flask_app:latest
빌드가 되었는지 확인을 한다
docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE my_flask_app latest 51fb98413e28 20 seconds ago 98.7MB
image를 container로 실행은 다음과 같다.
docker container run --name flask --publish 8080:5000 -d my_flask_app
fee755b1f3b34bff0b8d8f676a696976cfe4b24b9555b7257428c56ab799424d
구) 커맨드인 docker run
으로 해도 되지만 신명령어는 docker <<context>> <<command>>
형태를 사용한다.
즉 여기서는 docker container run
인 것이다. 커맨드도 매번 --help
를 보면 된다.
docker container --help
Usage: docker container COMMAND Manage containers Options: --help Print usage Commands: attach Attach local standard input, output, and error streams to a running container commit Create a new image from a container's changes cp Copy files/folders between a container and the local filesystem create Create a new container diff Inspect changes to files or directories on a container's filesystem exec Run a command in a running container export Export a container's filesystem as a tar archive inspect Display detailed information on one or more containers kill Kill one or more running containers logs Fetch the logs of a container ls List containers pause Pause all processes within one or more containers port List port mappings or a specific mapping for the container prune Remove all stopped containers rename Rename a container restart Restart one or more containers rm Remove one or more containers run Run a command in a new container start Start one or more stopped containers stats Display a live stream of container(s) resource usage statistics stop Stop one or more running containers top Display the running processes of a container unpause Unpause all processes within one or more containers update Update configuration of one or more containers wait Block until one or more containers stop, then print their exit codes Run 'docker container COMMAND --help' for more information on a command.
어쨌든 docker container run
이후 container id가 반환되었으면 성공적으로 실행된 것이다.
확인을 하면 동작중인것을 알 수 있다.
GET 127.0.0.1:8080
HTTP/1.0 200 OK Content-Type: text/html; charset=utf-8 Content-Length: 732 Server: Werkzeug/0.12.2 Python/3.6.3 Date: Sat, 07 Oct 2017 08:19:33 GMT <html> <head> <style type="text/css"> body { background: black; color: white; } div.container { max-width: 500px; margin: 100px auto; border: 20px solid white; padding: 10px; text-align: center; } h4 { text-transform: uppercase; } </style> </head> <body> <div class="container"> <h4>Cat Gif of the day</h4> <img src="http://ak-hdl.buzzfed.com/static/2013-10/enhanced/webdr05/15/9/anigif_enhanced-buzz-26358-1381845043-13.gif" /> <p><small>Courtesy: <a href="http://www.buzzfeed.com/copyranter/the-best-cat-gif-post-in-the-history-of-cat-gifs">Buzzfeed</a></small></p> </div> </body> </html>
아래와 같이 실행중인 컨테이너를 확인할 수 있다.
docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES fee755b1f3b3 my_flask_app "python app.py" 27 minutes ago Up 27 minutes 0.0.0.0:8080->5000/tcp flask
귀찮으니 force로 지운다
docker container rm -f flask
없어졌다.
docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
실제로 docker를 사용할때는 프로젝트를 시작하기전에 Dockerfile
과 docker-compose.yml
를 미리 만들어놓고 시작을 한다.
이제부터는 허접한 DevOps 라고 가정하고 다음과 같은 일을 해야 한다고 가정한다.
- drupal 8.2 버전을 설치한다
- drupal bootstrap theme을 설치한다
- port는 8080을 이용한다.
- database는 postgres 9.6 버전을 사용한다
- postgres password는 qwer1234qwer 로 한다
- 데이터는 container가 날아가도 보존하도록 한다
우선 가장 먼저 할일은 Dockerfile
을 만드는 일이다. 이 경우는 없어도 되긴 하지만 그냥 한다.
FROM drupal:8.2
RUN apt-get update \
&& apt-get install -y git \
&& rm -rf /var/lib/apt/lists*
WORKDIR /var/www/html/themes
RUN git clone --branch 8.x-3.x --single-branch --depth 1 https://git.drupal.org/project/bootstrap.git \
&& chown -R www-data:www-data bootstrap
WORKDIR /var/www/html
/var/www/html
이나 이런건 어떻게 알수 있는가? Dockerhub/drupal 에서 올려놓은 README
를 보면 된다.
docker-compose.yml
은 다음과 같다.
version: "3"
services:
drupal:
build: .
image: custom-drupal
depends_on:
- db
ports:
- 8080:80
volumes:
- drupal-modules:/var/www/html/modules
- drupal-profiles:/var/www/html/profiles
- drupal-sites:/var/www/html/sites
- drupal-themes:/var/www/html/themes
db:
image: postgres:9.6
environment:
POSTGRES_PASSWORD: qwer1234qwer
volumes:
- drupal-data:/var/lib/postgresql/data
volumes:
drupal-data:
drupal-modules:
drupal-profiles:
drupal-sites:
drupal-themes:
yaml
포맷을 사용한다.- volumes은 실제 호스트에 볼륨을 사용해 콘테이너가 없어지더라도 데이터가 남아있게 된다.
- 아무것도 작성하지 않으면
docker volume
을 사용한다.
- 아무것도 작성하지 않으면
environment
를 통해 환경변수를 설정할 수 있다.- 이런 secret들은 환경변수로 설정하는건 좋지 않다.
- 실제로는
docker secret
을 사용한다.
자세한 키워드는 Docker Compose Reference 에서 확인한다.
이제 실행은 docker-compose up
을 사용한다.
먼저 help를 살펴본다.
docker-compose --help
Define and run multi-container applications with Docker. Usage: docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...] docker-compose -h|--help Options: -f, --file FILE Specify an alternate compose file (default: docker-compose.yml) -p, --project-name NAME Specify an alternate project name (default: directory name) --verbose Show more output --no-ansi Do not print ANSI control characters -v, --version Print version and exit -H, --host HOST Daemon socket to connect to --tls Use TLS; implied by --tlsverify --tlscacert CA_PATH Trust certs signed only by this CA --tlscert CLIENT_CERT_PATH Path to TLS certificate file --tlskey TLS_KEY_PATH Path to TLS key file --tlsverify Use TLS and verify the remote --skip-hostname-check Don't check the daemon's hostname against the name specified in the client certificate (for example if your docker host is an IP address) --project-directory PATH Specify an alternate working directory (default: the path of the Compose file) Commands: build Build or rebuild services bundle Generate a Docker bundle from the Compose file config Validate and view the Compose file create Create services down Stop and remove containers, networks, images, and volumes events Receive real time events from containers exec Execute a command in a running container help Get help on a command images List images kill Kill containers logs View output from containers pause Pause services port Print the public port for a port binding ps List containers pull Pull service images push Push service images restart Restart services rm Remove stopped containers run Run a one-off command scale Set number of containers for a service start Start services stop Stop services top Display the running processes unpause Unpause services up Create and start containers version Show the Docker-Compose version information
이제 실행을 한다.
docker-compose up -d
docker container ls -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES 4d448b78b4fd custom-drupal "docker-php-entryp..." 19 seconds ago Up 18 seconds 0.0.0.0:8080->80/tcp drupal_drupal_1 4f2cf2a1722f postgres:9.6 "docker-entrypoint..." 19 seconds ago Up 18 seconds 5432/tcp drupal_db_1
이제 로컬호스트:8080으로 가보면
GET 127.0.0.1:8080
HTTP/1.1 302 Found Date: Sat, 07 Oct 2017 09:04:22 GMT Server: Apache/2.4.10 (Debian) X-Powered-By: PHP/7.1.5 Cache-Control: no-cache Location: /core/install.php Content-Length: 312 Content-Type: text/html; charset=UTF-8 <!DOCTYPE html> <html> <head> <meta charset="UTF-8" /> <meta http-equiv="refresh" content="1;url=/core/install.php" /> <title>Redirecting to /core/install.php</title> </head> <body> Redirecting to <a href="/core/install.php">/core/install.php</a>. </body> </html>
drupal 설치로 연결되는 것을 볼 수 있다.
중요한 점은 db를 선택하는 시점에
- postgreSQL 선택
- DATABASE HOST는
docker-compose.yml
에서 지정해준 이름인http://db
이다 - USERNAME은 건드리지 않았기 때문에 postgres 기본값인
postgres
이다 - PASSWORD는
qwer1234qwer
이다
실전으로 다음과 같은 architecture를 배포해야 되는 일인 경우,
docker-compose.yml
은 아래와 같다.
version: "3"
services:
vote:
image: dockersamples/examplevotingapp_vote:before
depends_on:
- redis
deploy:
replicas: 2
update_config:
parallelism: 2
restart_policy:
condition: on-failure
ports:
- 80:80
networks:
- frontend
redis:
image: redis:3.2
deploy:
replicas: 2
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
networks:
- frontend
worker:
image: dockersamples/examplevotingapp_worker
deploy:
placement:
constraints: [node.role == manager]
replicas: 1
labels: [APP=VOTING]
restart_policy:
condition: on-failure
delay: 10s
max_attempts: 3
window: 120s
networks:
- frontend
- backend
db:
image: postgres:9.4
volumes:
- db-data:/var/lib/postgresql/data
deploy:
replicas: 1
placement:
constraints: [node.role == manager]
networks:
- backend
result:
image: dockersamples/examplevotingapp_result:before
ports:
- 5001:80
networks:
- backend
deploy:
replicas: 1
update_config:
parallelism: 2
delay: 10s
restart_policy:
condition: on-failure
depends_on:
- db
visualizer:
image: dockersamples/visualizer:stable
ports:
- 8080:8080
stop_grace_period: 1m30s
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
deploy:
placement:
constraints: [node.role == manager]
networks:
frontend:
backend:
volumes:
db-data:
networks:
frontend:
backend:
이 부분은 docker network
로 네트워크가 저절로 생성 된후 각각의 콘테이너가 속하는 오버레이에서만 서로 통신이 가능하다.
docker network create --driver overlay frontend
docker network create --driver overlay backend
실제 배포를 할 때는 docker swarm
을 이용해 여러대의 노드로 클러스터를 구성한다음
docker stack deploy -c docker-compose.yml voteapp
이전 docker-compose.yml
에서 deploy
에 들어간 옵션들이 docker stack deploy
를 할때 적용되는 옵션들인 것이다.
결론은
docker-compose
는 development 할 때 사용하는 명령어docker stack deploy
는 실제 배포할 때 사용 되는 명령어docker stack deploy
를 할 경우 Dockerfile로 빌드를 못한다- 배포 이전에 내 이미지를 만들어
registry
로 모든 노드에 접속할 수 있게 해준다.
- 배포 이전에 내 이미지를 만들어
registry는 로컬 전용 dockerhub 라고 생각하면 된다. 스웜 클러스터에 자동으로 공유가 된다.
다음과 같이 사용한다.
docker service create --name registry -p 5000:5000 registry
docker tag my_app 127.0.0.1:5000/my_app
docker push 127.0.0.1:5000/my_app
이제 모든 노드에서 다음과 같이 내 이미지를 다운 받을 수 있다.
docker service create --name my_app 127.0.0.1:5000/my_app
docker secret create psql_user psql_user.txt
혹은
echo "postgresuser" | docker secret create psql_user -
로 생성한다. (끝에 -
가 중요하다)
실제 컨테이너에서는 /run/secrets/psql_user
로 접근이 가능하다. 파일처럼 보이지만 파일이 아니라 메모리에 저장되어있다.
services:
psql:
image: postgres
environment:
POSTGRES_USER_FILE: /run/secrets/psql_user
POSTGRES_PASSWORD_FILE: /run/secrets/psql_pass
secrets:
- psql_user
- psql_pass
secrets:
psql_user:
file: ./psql_user.txt
psql_pass:
file: ./psql_pass.txt
으로 사용한다. 하지만 보안 이슈로 파일로 저장하기보다 external: true
세팅을 한 후 docker secret create
으로 생성한다.
secrets:
psql_user:
external: true
psql_pass:
external: true
다음과 같은 파일로 관리를 한다.
- docker-compose.yml
- 이미지만 설정되어있는 베이스 파일
- docker-compose.override.yml
- 개발시 사용되는 config
- docker-compose.prod.yml
- 실제 배포시 사용되는 config
- docker-compose.test.yml
- CI에서 사용되는 config
docker-compose.yml
version: '3.1'
services:
drupal:
image: drupal:latest
postgres:
image: postgres:9.6
docker-compose.override.yml
version: '3.1'
services:
drupal:
build: .
ports:
- "8080:80"
volumes:
- drupal-modules:/var/www/html/modules
- drupal-profiles:/var/www/html/profiles
- drupal-sites:/var/www/html/sites
- ./themes:/var/www/html/themes
postgres:
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/psql-pw
secrets:
- psql-pw
volumes:
- drupal-data:/var/lib/postgresql/data
volumes:
drupal-data:
drupal-modules:
drupal-profiles:
drupal-sites:
drupal-themes:
secrets:
psql-pw:
file: psql-fake-password.txt
docker-compose.prod.yml
version: '3.1'
services:
drupal:
ports:
- "80:80"
volumes:
- drupal-modules:/var/www/html/modules
- drupal-profiles:/var/www/html/profiles
- drupal-sites:/var/www/html/sites
- drupal-themes:/var/www/html/themes
postgres:
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/psql-pw
secrets:
- psql-pw
volumes:
- drupal-data:/var/lib/postgresql/data
volumes:
drupal-data:
drupal-modules:
drupal-profiles:
drupal-sites:
drupal-themes:
secrets:
psql-pw:
external: true
docker-compose.test.yml
version: '3.1'
services:
drupal:
image: drupal
build: .
ports:
- "80:80"
postgres:
environment:
- POSTGRES_PASSWORD_FILE=/run/secrets/psql-pw
secrets:
- psql-pw
volumes:
- ./sample-data:/var/lib/postgresql/data
secrets:
psql-pw:
file: psql-fake-password.txt
# if you're doing anything beyond your local machine, please pin this to a specific version at https://hub.docker.com/_/node/
FROM node:6
RUN mkdir -p /opt/app
# set our node environment, either development or production
# defaults to production, compose overrides this to development on build and run
ARG NODE_ENV=production
ENV NODE_ENV $NODE_ENV
# default to port 80 for node, and 5858 or 9229 for debug
ARG PORT=80
ENV PORT $PORT
EXPOSE $PORT 5858 9229
# check every 30s to ensure this service returns HTTP 200
HEALTHCHECK CMD curl -fs http://localhost:$PORT/healthz || exit 1
# install dependencies first, in a different location for easier app bind mounting for local development
WORKDIR /opt
COPY package.json /opt
RUN npm install && npm cache clean --force
ENV PATH /opt/node_modules/.bin:$PATH
# copy in our source code last, as it changes the most
WORKDIR /opt/app
COPY . /opt/app
# if you want to use npm start instead, then use `docker run --init in production`
# so that signals are passed properly. Note the code in index.js is needed to catch Docker signals
# using node here is still more graceful stopping then npm with --init afaik
# I still can't come up with a good production way to run with npm and graceful shutdown
CMD [ "node", "index.js" ]
FROM ubuntu:latest
# Elixir requires UTF-8
RUN apt-get update && apt-get upgrade -y && apt-get install locales && locale-gen en_US.UTF-8
ENV LANG en_US.UTF-8
ENV LANGUAGE en_US:en
ENV LC_ALL en_US.UTF-8
# update and install software
RUN apt-get install -y curl wget git make sudo \
# download and install Erlang apt repo package
&& wget https://packages.erlang-solutions.com/erlang-solutions_1.0_all.deb \
&& dpkg -i erlang-solutions_1.0_all.deb \
&& apt-get update \
&& rm erlang-solutions_1.0_all.deb \
# For some reason, installing Elixir tries to remove this file
# and if it doesn't exist, Elixir won't install. So, we create it.
# Thanks Daniel Berkompas for this tip.
# http://blog.danielberkompas.com
&& touch /etc/init.d/couchdb \
# install latest elixir package
&& apt-get install -y elixir erlang-dev erlang-dialyzer erlang-parsetools \
# clean up after ourselves
&& apt-get clean
# install the Phoenix Mix archive
RUN mix archive.install --force https://github.com/phoenixframework/archives/raw/master/phoenix_new.ez
RUN mix local.hex --force \
&& mix local.rebar --force
# install Node.js (>= 8.0.0) and NPM in order to satisfy brunch.io dependencies
# See http://www.phoenixframework.org/docs/installation#section-node-js-5-0-0-
RUN curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash - && sudo apt-get install -y nodejs
WORKDIR /code
COPY . /code
RUN mix deps.get && npm install && npm cache clean --force
EXPOSE 80 4000 4001