# Docker
* Docker là công cụ giúp ta có thể chạy project ở một môi trường cụ thể, được định sẵn rõ ràng, độc lập với môi trường gốc. Các ứng dụng chạy trong Docker được gọi là các Container ("Cờn tên nơ")
* 
* Kubernetes: Tool để quản lý các project chạy bằng Docker, tự động Heal khi có lỗi, tự động scale, tự động deploy, tự động và tự động
## Các khái niệm:
### Dockerfile

* Là bản vẽ thiết kế của
### Docker Image
* Như 1 mô hình 3d, ý tưởng của ngôi nhà mà có thể share với những người khác
### Docker Container
* Dựa vào mô hình đấy, ta run và có căn nhà thật

### Docker compose
* Để các căn nhà kết nối với nhau tạo này 1 neighborhood hay kết hợp các thành phần trong căn nhà thì ta cần compose

* **Link các khái niệm: https://viblo.asia/p/li-do-toi-yeu-docker-ORNZqxRMK0n**
## Dockerize NodeJS app
### Build image
#### Viết dockerFile
```yaml
FROM node:latest
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "start"]
```
* Ở trên ta có 1 dockerfile có những thành phần:
* FROM: môi trường ta build image trên (môi trường này đã được build thành image)
* WORKDIR: Trong image, tạo 1 folder app và dẫn vào (mkdir app && cd app)
* COPY: đưa/copy toàn bộ file code từ host vào thư mục app trong image (sau đó, chạy lệnh trên những file code này)
* RUN: chạy để install các dependencies dựa vào file ta đã copy vào
* CMD: luôn được chạy khi khởi tạo container
* Ta có thể đọc thêm về các linux distribution và lựa chọn môi trường image phù hợp để có thể giảm size của image ta build. ví dụ: thay vì lastest ta chọn linux alpine, node version 13.
```yaml
FROM node:13-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "start"]
```
#### Build docker image
> docker build -t learning-docker/node:v1 .
* Build docker image dùng **docker build****docker build**
* -t : tên của image (ví dụ: tên là learning-docker/node với version là v1)
* Dấu . phía của nói rằng hãy tìm dockerfile trong thư mục/ đường dẫn hiện tại để build.
##### Các steps khi chạy build image
* 
* show all images:
> docker images
* Xóa images:
> docker rmi <image id/name>
### Build container
#### Viết docker compose file
```yaml
version: "3.7"
services:
app:
image: learning-docker/node:v1
ports:
- "3000:3000"
restart: unless-stopped
```
* Tạo file docker-compose.yml
* Docker compose file có các thành phần:
* Version: version của file (chọn mới nhất như 3.8)
* services: Các component/ microservices của project (như db, fe, be, api,...)
* Trong các services/ tòa nhà thì ta phải định nghĩa cho thằng thợ biết nó được xây từ mô hình 3d nào (image)
* restart: unless-stopped để nói cứ restart/chạy nó đừng stop nhưng nếu nó gặp lỗi hay dừng bằng thằng ráp lego thì đừng restart nữa.
* Nếu chỉ chạy những thành phần trên thì ko thể truy cập dc app ta từ http/localhost. Ở trong container thì port 3000 là port của node app, ta muốn truy cập từ bên ngoài thì phải tạo 1 port 3000 để chọc vào.
#### Additional
* Chạy docker-compose file:
> docker compose up
* Chạy ko làm treo terminal:
> docker compose up -d
* Xem log:
> docker compose logs
> docker compose logs -f : check realtime
* Dừng docker-compose file:
> docker compose down
* Truy cập bên trong container
* App là tên service/ container ta muốn vào
> docker compose exec app sh
* List file
> ls -l
* xem hệ điều hành
> cat /etc/os-release
* Copy 1 hoặc 1 số file
```
COPY app.js . # Copy app.js ở folder hiện tại vào đường dẫn ta set ở WORKDIR trong Image
COPY app.js /abc/app.js ## Kết quả tương tự, ở đây ta nói rõ ràng hơn (nếu ta muốn copy tới một chỗ nào khác không phải WORKDIR)
# Copy nhiều file
COPY app.js package.json package-lock.json .
# Ở trên ta các bạn có thể copy bao nhiêu file cũng được, phần tử cuối cùng (dấu "chấm") là đích ta muốn copy tới trong Image
```
* ADD:
* Cũng dùng để copy từ 1 nơi vào image
* Có thể copy từ URL hoặc giải nén nhưng
* Best practice: dùng copy và dùng thêm RUN
* RUN curl/wget .... : dowload file
* RUN tar -xvzf ... : giải nén file
* ENTRYPOINT:
* 1 kỹ thuật nâng cao chạy CMD
* Dùng cấu hình CMD trc khi chạy CMD
* TH thường dùng với ENTRYPOINT dùng 1 file shell script .sh để cấu hình tất tần tật những thứ cần thiết trước khi khởi chạy container bằng CMD
```yaml
ENTRYPOINT ["sh", "/var/www/html/.docker/docker-entrypoint.sh"]
CMD supervisord -n -c /etc/supervisord.conf
```
## Dockerize Python/Flask app
### Build image
#### Viết dockerfile
```yaml
FROM python:3.6-alpine
WORKDIR /app
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
```
* 1 số thành phần:
* FROM: lấy hđh alpine linux, python 3.6
* RUN: Chạy pip install để cài các dependencies
* CMD: chạy file app.py khi chạy container
#### Build image
> docker build -t learning-docker/python:v1 .
### Run container
#### Viết docker-compose.yml
```yaml
version: "3.7"
services:
app:
image: learning-docker/python:v1
ports:
- "5000:5000"
restart: unless-stopped
```
* các thành phần:
* version: 3.7
* services: có 1 là app
* app: dc xây từ image ta đã tạo
* port trong container là 5000, ta tạo 1 port 5000 ở ngoài xọt vào
* restart: cũng vậy
> docker compose up

* Và nó bị lỗi:

* Doc từ flask:
Nếu bạn chạy ứng dụng lên thì bạn sẽ để ý thấy rằng ứng dụng của bạn chỉ có thể truy cập được trong phạm vi máy của bạn (localhost), điều này được cài đặt mặc định
* chỉ môi trường trong container mới truy cập được vào project, project của chúng ta coi môi trường đó mới là localhost, còn từ môi trường gốc (bên ngoài) truy vấn thì sẽ không được gọi là localhost nữa.
* Fix:
```py
from flask import Flask, render_template
app = Flask(__name__)
@app.route("/")
def hello():
return render_template('index.html', title='Docker Python', name='James')
if __name__ == "__main__":
app.run(host="0.0.0.0")
```
* host=0.0.0.0 để nói với project chúng ta là "chấp nhận cho tất cả mọi IP truy cập" => local host khi này là 0.0.0.0
* Sau đó ta build image, run container lại là ok
#### Additional
##### ENV (biến môi trường)
* khi chạy thật tế sử dụng biến môi trường sẽ giúp ta rất nhiều trong việc giảm tối thiểu việc phải sửa code
* Có thể đặt biến môi trường trong dockerfile hoặc docker-compose
##### Dockerfile
```dockerfile
FROM python:3.6-alpine
WORKDIR /app
# Tạo ra biến môi trường tên là PORT với giá trị 5555
ENV PORT 5555
COPY . .
RUN pip install -r requirements.txt
CMD ["python", "app.py"]
```
```py
from flask import Flask, render_template
import os
app = Flask(__name__)
@app.route("/")
def hello():
return render_template('index.html', title='Docker Python', name='James')
if __name__ == "__main__":
app.run(host="0.0.0.0", port=os.environ['PORT']) # Chạy project ở PORT nhận vào từ biến môi trường
```
##### Docker compose
* Biến môi trường ở file Dockerfile sẽ được khai báo khi ta build image
* Biến môi trường ở file docker-compose.yml sẽ được khởi tạo khi container được khởi tạo, tức là khi ta chạy docker-compose up. Do đó để thay đổi biến môi trường ta chỉ cần down và up là xong
```yaml
version: "3.7"
services:
app:
image: learning-docker/python:v1
ports:
- "5000:6666"
restart: unless-stopped
environment:
PORT: 6666
```
##### Dùng .env file (advanced)
* file .env
```
PORT=8888
PUBLIC_PORT=9999
```
* docker-compose.yml
```yaml
version: "3.4"
services:
app:
image: learning-docker/python:v1
ports:
- "${PUBLIC_PORT}:${PORT}"
restart: unless-stopped
environment:
PORT: ${PORT}
```
#### Push and pull image from register
* là nơi ta lưu Docker image (giống như github để lưu code, nhưng đây là lưu Docker image), có rất nhiều registry, có public có private.
* push to gitlab:
* login:
> docker login registry.gitlab.com
* Đổi tên image theo format của gitlab:
> docker tag learning-docker/python:v1 registry.gitlab.com/tiennguyen/learning-docker
* push thôi:
> docker push registry.gitlab.com/tiennguyen/learning-docker
* pull image:
* Tạo 1 folder ex: test-docker
* Tạo 2 file .env và docker compose:
* .env có các ports như cũ
* docker compose:
* Docker compose up
```yaml
version: "3.4"
services:
app:
image: registry.gitlab.com/tiennguyen/learning-docker
ports:
- "${PUBLIC_PORT}:${PORT}"
restart: unless-stopped
environment:
PORT: ${PORT}
```
## Dockerize VueJS, ReactJS
### Build image
#### Viết dockerfile
```dockerfile
# build stage
FROM node:16-alpine as build-stage
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
## các bạn có thể dùng yarn install .... tuỳ nhu cầu nhé
# production stage
FROM nginx:1.17-alpine as production-stage
COPY --from=build-stage /app/dist /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
```
* các thành phần trong dockerfile:
* Có 2 giai đoạn là build và production stages
* Build stage:
* FROM: có thêm as để chỉ giai đoạn hiện tại
* RUN npm install để cài dependencies
* RUN npm run build để build project, sau khi build thì có 1 folder sinh ra sau khi build và sẽ dùng nó ở giai đoạn sau.
* Production stage:
* FROM: lấy image của nginx version 1.17 và linux alpine
* COPY:
* --from= build-stage: lấy folder từ build stage
* app/dist: những file build cuối cùng cần thiết để chạy ở trình duyệt
* /usr/share/nginx/html: nơi Nginx sẽ tìm tới và trả về cho user khi user truy cập ở trình duyệt
* CMD: khởi động nginx
#### Build image
> docker build -t learning-docker/vue:v1 .
### Run container
#### Viết Docker-compose.yml
```yaml
version: "3.7"
services:
app:
image: learning-docker/vue:v1
ports:
- "5000:80"
restart: unless-stopped
```
* Port mặc định trong container của nginx là port 80
> docker compose up -d
#### Dockerfile of ReactJS
```dockerfile
# build stage
FROM node:16-alpine as build-stage
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
# production stage
FROM nginx:1.17-alpine as production-stage
COPY --from=build-stage /app/build /usr/share/nginx/html
CMD ["nginx", "-g", "daemon off;"]
```
### Container trung gian (temporary container)
* Container này sau khi làm xong nhiệm vụ thì sẽ tự được xoá đi.
```dockerfile
# MacOS + Linux
docker run --rm -v $(pwd):/app -w /app node:16-alpine npm install && npm run build
# Nếu bạn đang dùng Windows thì command trên sẽ như sau:
# Với Git bash
docker run --rm -v "/$(pwd)":/app -w //app node:16-alpine npm install && npm run build
# Với PowerShell
docker run --rm -v "$(pwd):/app" -w /app node:16-alpine npm install && npm run build
# Với Command Prompt
docker run --rm -v "%cd%:/app" -w /app node:16-alpine npm install && npm run build
```
* Chạy câu lệnh trên ở folder prj
* Các thành phần:
* --rm: chạy xong thì tự xóa
* -v: volume
* $(pwd):/app: mount toàn bộ file ở folder hiện tại vào folder app trong image
* -w /app: workdir is app
* node:16-alpine: FROM node:16-alpine
* npm install && npm run build: install và build trong image
* Mục đích của việc làm trên là ta tạo được những folder node_modules và dist ở trong containain nhờ volume, ta ánh xạ/ mang chúng được ra ngoài folder ở host và dùng luôn (coi như ko cần stage để build các folder đó nữa)
```yaml
version: "3.4"
services:
app:
image: nginx:1.17-alpine
volumes:
- ./dist:/usr/share/nginx/html
ports:
- "5000:80"
restart: unless-stopped
```
* Các thành phần:
* image: dùng image của nginx, ko dùng image đã build trc
* volumes: ánh xạ **nội dung bên trong** folder dist vào image nginx
* ./dist: folder dist sau khi đã được ánh xạ từ temporary container
* /usr/share/nginx/html: nơi ngnix cần
##### Có 2 dạng ánh xạ thường:
* mapping port
* mount volume
* vế trái luôn là ở môi trường gốc (bên ngoài), vế phải là bên trong container
## Dockerize Laravel
### Build image
#### Viết dockerfile
```dockerfile
# Set master image
FROM php:7.2-fpm-alpine
# Set working directory
WORKDIR /var/www/html
# Install PHP Composer
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
# Copy existing application directory
COPY . .
```
* Cấu hình dockerfile cho php-fpm
* Các thành phần:
* RUN curl: cài fpm để install các thư viện php
* image php:7.2-fpm-alpine đã chạy CMD cho chúng ta rồi, nên khi chạy Docker sẽ thấy là "container vẫn luôn trong trạng thái hoạt động",ta không cần CMD nữa.
```yaml
EXPOSE 9000
CMD ["php-fpm"]
```
#### build image
> docker build -t learning-docker/laravel:v1 .
### Run container
#### Viết docker compose file
```yaml
version: '3.4'
services:
#PHP Service
app:
image: learning-docker/laravel:v1
restart: unless-stopped
volumes:
- ./:/var/www/html
#Nginx Service
webserver:
image: nginx:1.17-alpine
restart: unless-stopped
ports:
- "8000:80"
volumes:
- ./:/var/www/html
- ./nginx.conf:/etc/nginx/conf.d/default.conf
```
* Các thành phần:
* PHP service:
* image: dùng image ta đã build
* volumes: lấy all files ở folder hiện tại mount vào /var/www/html
* Nginx service:
* ports: dùng port 8000 chọt vào port 80
* volume:
* ./:/var/www/html: giống PHP
* ./nginx.conf:/etc/nginx/conf.d/default.conf: mount file ngnix config ở host vào container
* Vì để nginx có thể hiểu và vận hành php nên ta cần phải có file config để nginx đọc và làm theo
#### Cấu hình ngnix (Viết nginx.conf)
```nginx
server {
listen 80;
index index.php index.html;
error_log /var/log/nginx/error.log;
access_log /var/log/nginx/access.log;
root /var/www/html/public;
location ~ \.php$ {
try_files $uri =404;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass app:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_hide_header X-Powered-By;
}
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}
```
* Trên file trên có dòng này:
> fastcgi_pass app:9000;
* Note: khi có request gửi đến nginx qua port 8000 thì request sẽ được gửi đến service app ở port 9000 (PHP-FPM)
* Ở file docker compose ko map port nhưng trong image build sẵn đã EXPOSE 9000 => Các container khác ex:nginx có thể giao tiếp với service app (PHP-FPM) qua port 9000 (giao tiếp trong network)
#### Phân biệt map port và export/expose port
* Map port để bên ngoài giao tiếp với container
* Export port để các container giao tiếp với nhau (Ex: service app đã expose port 9000 để nginx có thể gọi vào, gửi request ; nginx ko cần expose port vì ko container nào gọi đến nó)
* Ở bên ngoài ko thể gọi app ở 9000
##### Chạy composer install
* có 2 cách:
* Chui vào trong container app
* Đứng ở ngoài và chạy
> docker compose exec app composer install
##### Generate key
> docker compose exec app php artisan key:generate
* Nếu thành công sẽ ra này

#### Additional
* Các thành phần của ứng dụng trên:
* PHP không phải cứ thế ăn ngay chạy luôn được, cần phải có một anh bạn đảm nhận nhiệm vụ thực thi code PHP và ta dùng PHP-FPM
* Nginx trong việc vận hành ứng dụng PHP (Laravel).
* ứng dụng thành 2 phần:
* PHP-FPM, trong đó có cài tất cả mọi thứ liên quan như composer, thư viện, setup, vì php-fpm đảm nhiệm vai trò chính trong việc chạy code
* webserver Nginx đóng vai trò như là 1 anh gác cửa, đứng ở bên ngoài, khi có request gửi đến anh gác cửa anh ấy sẽ làm một số nhiệm vụ và chuyển request vào cho php-fpm ở bên trong xử lý
#### Fix Permission Denied
> docker compose exec app sh
top
* Dùng để check file permission
* mount toàn bộ code ở môi trường gốc vào trong service app do vậy toàn bộ code thực tế trong container sẽ ăn theo permission với môi trường ngoài
* ở môi trường ngoài ta chạy:
> sudo chwown -R 82:82 .
* ta nên đổi permission code ở môi trường ngoài cho khớp với user thực tế chạy trong container để tránh lỗi sau này
#### Tại sao không chạy composer install ngay lúc build image?
* Khi ta mount volume của service app từ bên ngoài vào trong container thì khi container được khởi tạo, toàn bộ file từ bên ngoài sẽ được ghi đè vào bên trong container, dẫn tới việc folder vendor (do composer install mà có) sẽ bị biến mất trong container, vì ở môi trường ngoài ta đâu có vendor
#### .dockerignore
* Trong bài này thì ta ko cần nginx.conf ở PHP service nên khi ta COPY thì dư thừa nên ta tạo file ignore và đưa nó vào, giảm size image xuống
## Dockerize Project NodeJS, MongoDB, Redis, Passport
### Tổng quan
* Project này kiến trúc có những gì:
* Ta chỉ có 2 model là User và Product
* Dùng MongoDB để lưu trữ dữ liệu
* Dùng Redis để lưu trữ session của user đăng nhập
* Để xử lý Login/Logout ta dùng PassportJS
* Để xử lý upload file ta dùng Multer
* Chia các services trong docker compose file:
* Có MongoDB là database -> ta có service db, dùng image mongo được build sẵn
* Có redis để lưu session của user -> ta có serivce redis, dùng image redis được build sẵn
* Phần còn lại là serivce để chạy project nodejs, kết nối tới 2 service bên trên -> cấu hình dockerfile cho service này, ta gọi là app
### Build image
#### Viết dockerfile
```dockerfile
FROM node:16-alpine
WORKDIR /app
COPY . .
RUN npm install
# Development
CMD ["npm", "run", "dev"]
# Production
# RUN npm install -g pm2
# CMD ["pm2-runtime", "ecosystem.config.js", "--env", "production"]
```
* Các thành phần:
* CMD: dùng nodemon để run prj ở môi trường dev vì cần cập nhật liên tục còn ở production thì ko nên ta sẽ chạy đoạn code dưới khi cần (build image cho mtr dev)
#### Docker build
> docker build -t learning-docker/docker-node-mongo-redis:v1 .
### Run project/ contain
#### Data persistent: lưu lại dữ liệu
* Khi ta chạy prj trong docker container thì khi restart toàn bộ data tạo ra sẽ mất theo (Ex: trong prj này có data từ db và redis)
* Để lưu lại thì tiếp tục dùng mount/ ánh xạ data, khi restart thì toàn bộ data sẽ dc chuyển lại vào container.
* Ở folder gốc, tạo folder .docker. Trong .docker ta tạo folder data, trong data ta tạo 2 folder tên là db (cho mongodb) và redis cho redis
#### Viết docker compose file
```yaml
version: "3.4"
services:
app:
image: learning-docker/docker-node-mongo-redis:v1
volumes:
- ./:/app # mount từ môi trường gốc vào trong để nếu các bạn thay đổi code thì bên trong sẽ tự động cập nhật
environment: # phần này ta định nghĩa ở file .env nhé
- DB_HOST=${DB_HOST}
- DB_NAME=${DB_NAME}
- REDIS_HOST=${REDIS_HOST}
- REDIS_PORT=${REDIS_PORT}
- PORT=${PORT}
ports:
- "${PORT}:${PORT}" # phần này ta định nghĩa ở file .env nhé
restart: unless-stopped
depends_on:
- redis
- db
db:
image: mongo:4.4
volumes:
- .docker/data/db:/data/db
restart: unless-stopped
redis:
image: redis:5-alpine
volumes:
- .docker/data/redis:/data
restart: unless-stopped
```
* Các thành phần:
* service mongo, redis:
* volumes: mount từ .docker/data/db vào /data/db
* khi khởi chạy Mongo sẽ tự tìm đến /data/db và load những dữ liệu vào trong database
* service app:
* image: lấy image đã build
* volumes: ./:/app mount từ folder vào app, nếu mtr ngoài thay đổi thì trong image sẽ cập nhật theo (giống COPY nhưng này realtime)
* Các thành phần env thì có file .env
* depends_on: service app sẽ phụ thuộc vào 2 services là db và redis
* docker compose up => db và redis khởi động trc app
* docker compose up app => đồng thời sẽ tạo ra 2 service db và redis
* docker compose stop => app sẽ bị stop trước 2 service kia
* service db khởi động trc app nhưng nó có thể mất 1 khoảng thời gian để app connect vào (dùng connectionRetry để thử kết nối từ nodeJS vào Mongo)
#### Additional
* Các services chạy khác HĐH, có service chạy alpine, có cái chạy ubuntu => vẫn dùng cùng dc
#### Fix npm install
```yaml
# MacOS + Linux
docker run --rm -v $(pwd):/app -w /app node:16-alpine npm install
# Nếu bạn đang dùng Windows thì command trên sẽ như sau:
# Với Git bash
docker run --rm -v "/$(pwd)":/app -w //app node:16-alpine npm install
# Với PowerShell
docker run --rm -v "$(pwd):/app" -w /app node:16-alpine npm install
# Với Command Prompt
docker run --rm -v "%cd%:/app" -w /app node:16-alpine npm install
```
* Vì ta đã mount toàn bộ ở host vào trong app ở docker compose file nhưng host ko có node_modules, để có thì phải run npm install => Cần tạo temporary container
### Deploy
#### Build Image cho production
##### Viết dockerfile
```dockerfile
FROM node:16-alpine
WORKDIR /app
COPY . .
RUN npm install
# Development
# CMD ["npm", "run", "dev"]
# Production
RUN npm install -g pm2
CMD ["pm2-runtime", "ecosystem.config.js", "--env", "production"]
```
* các thành phần:
* RUN npm install -g pm2
* PM2 (process manager) là một tool để ta chạy và quản lý ứng dụng NodeJS
* Cấu hình PM2 ở ecosystem.config.js
* tạo .dockerignore:
```dockerfile
# Bỏ qua node_modules vì lát nữa ở Dockerfile sẽ chạy npm install
node_modules
# Bỏ qua folder .docker vì folder này chỉ có service db và redis mới cần (ta đang build image cho service app)
.docker
# Bỏ qua ảnh mà chúng ta đã upload lên trong quá trình test
public/images/*
# Bỏ qua file .env, file này dùng cho docker-compose khi chạy nên không cần đưa vào image
.env
# Bỏ qua Dockerfile không đưa vào image vì file này chỉ dùng tại thời điểm build image
Dockerfile
# Bỏ qua file docker-compose vì file này chỉ dùng tại thời điểm khởi chạy project
docker-compose.yml
```
#### Build image
> docker build -t learning-docker/docker-node-mongo-redis:production .
#### Push image lên registry
```dockerfile
docker tag learning-docker/docker-node-mongo-redis:production registry.gitlab.com/<username>/<tên repo>:node_mongo_redis_production
# Ví dụ của mình
docker tag learning-docker/docker-node-mongo-redis:production registry.gitlab.com/tiennguyen/learning-docker:node_mongo_redis_production
```
#### Deploy
* Tạo folder test-deploy có docker-compose.yml
```yaml
version: "3.4"
services:
app:
image: registry.gitlab.com/maitrungduc1410/learning-docker:node_mongo_redis_production
volumes:
- ./public/images:/app/public/images # ta chỉ cần mount mỗi folder image thôi nhé
environment: # phần này ta định nghĩa ở file .env nhé
- DB_HOST=${DB_HOST}
- DB_NAME=${DB_NAME}
- REDIS_HOST=${REDIS_HOST}
- REDIS_PORT=${REDIS_PORT}
- PORT=${PORT}
ports:
- "${PORT}:${PORT}" # phần này ta định nghĩa ở file .env nhé
restart: unless-stopped
depends_on:
- redis
- db
db:
image: mongo:4.4
volumes:
- .docker/data/db:/data/db
restart: unless-stopped
redis:
image: redis:5-alpine
volumes:
- .docker/data/redis:/data
restart: unless-stopped
```
* File .env :
```yaml
PORT=3000
DB_HOST=db
DB_PORT=27017
DB_NAME=my_db
REDIS_HOST=redis
REDIS_PORT=6379
```
> docker compose up -d
* Khi chạy thì docker compose sẽ check những volume ta mount, nếu chưa có thì sẽ tự tạo
#### Điều hay ho của Docker
* giả sử sau khi bạn đã deploy thành công project chạy thật ngon lành rồi, mà có lí do nào đó bạn cần phải chuyển sang server khác, Thì việc bạn cần làm là copy nguyên cái "đống" 😃 bên trên, tức folder test-deploy mà ta làm ở phần trên, và copy sang server mới, sau đó dùng 1 command:
> docker compose up -d
## Docker Network và HEALTHCHECK
### Network trong Docker
* Các container/service trong docker có thể giao tiếp với nhau, cũng có thể kết nối với cả service bên ngoài, tạo 1 ứng dụng có nhiều thành phần, dễ dàng tăng giảm số lượng thành phần
* network giúp ứng dụng của ta cấu trúc tốt hơn, rõ ràng hơn về sự quan hệ giữa các thành phần trong ứng dụng, và quan trọng là cả bảo mật tốt hơn nữa.
### Các containers giao tiếp với nhau
* Cần phải triển khai trên cùng network (EX: ta có 3 service app, db, redis mặc định chúng được chạy trên cùng 1 network, và các service như db hay redis đã EXPOSE các port như 27017 và 6379, nhờ thế mà service app có thể gọi tới được.)
> docker compose up -d

* Đầu tiên, tạo 1 network, sau đó khởi động các services tạo containers và join vào network đó.
### Lý do:
* Làm rõ sự quan hệ giữa các service/container, nhìn vào network ta có thể thấy được 1 service hoạt động cần có sự tham gia của những service nào. EX: db hoạt động tách biệt redis nhưng ta ko thấy ở docker compose file.
* Điều thứ 2 và quan trọng hơn là dùng network sẽ giúp ứng dụng của ta bảo mật hơn, 1 service chỉ có thể giao tiếp với 1 số service nhất định (do ta định nghĩa) EX: khi hack chiếm được service redis thì hacker cũng có thể truy cập tới service db và thực hiện tấn công tiếp service db
### Cấu hình cho prj
#### Phân tích
* ta có 3 service app, db, và redis:
* Service app cần có 2 service db và redis để có thể hoạt động được (1 cho lưu trữ data, 1 cho lưu trữ session của user)
* Service db và redis có thể hoạt động độc lập không liên quan gì tới nhau, nên chúng không cần giao tiếp với nhau.
* ta sẽ tạo ra 2 network:
* db-network: dùng cho service app và service db nhằm mục đích trao đổi dữ liệu trong database
* redis-network: dùng cho service app và redis trong việc lưu session của user
```yaml
version: "3.4"
services:
app:
image: learning-docker/docker-node-mongo-redis:production
volumes:
- ./public/images:/app/public/images
environment:
- DB_HOST=${DB_HOST}
- DB_NAME=${DB_NAME}
- REDIS_HOST=${REDIS_HOST}
- REDIS_PORT=${REDIS_PORT}
- PORT=${PORT}
ports:
- "${PORT}:${PORT}"
restart: unless-stopped
depends_on:
- redis
- db
networks:
- db-network
- redis-network
db:
image: mongo:4.4
volumes:
- .docker/data/db:/data/db
restart: unless-stopped
networks:
- db-network
redis:
image: redis:5-alpine
volumes:
- .docker/data/redis:/data
restart: unless-stopped
- redis-network
networks:
- redis-network
#Docker Networks
networks:
db-network:
driver: bridge
redis-network:
driver: bridge
```
* Các thành phần:
* driver: là 1 Kiểu network
* Docker cung cấp cho chúng ta 1 số driver như:
* bridge:
* driver mặc định
* Driver này thường dùng khi ứng dụng của chúng ta cấu thành từ các container riêng biệt và chúng cần phải giao tiếp với nhau
* host, overlay, macvlan hay none: dùng cho Docker Swarm (Mình dùng k8s)
* Thành quả:
> docker compose up -d

* Docker tạo 2 networks, sau đó tạo container và cho chúng join vào các networks này
#### TEST
> docker-compose exec app sh
#Tiếp theo ta cài CURL để tạo request và nhìn cho trực quan nhé
apk add curl
* Chui vào container app và connect đến 2 service
> curl db:27017

> curl redis:6379

> docker-compose exec redis sh
apk add curl
curl db:27017

### Docker HEALTHCHECK
#### Viết docker compose file
```yaml
version: "3.4"
services:
app:
image: learning-docker/docker-node-mongo-redis:production
volumes:
- ./public/images:/app/public/images
environment: # phần này ta định nghĩa ở file .env nhé
- DB_HOST=${DB_HOST}
- DB_NAME=${DB_NAME}
- REDIS_HOST=${REDIS_HOST}
- REDIS_PORT=${REDIS_PORT}
- PORT=${PORT}
ports:
- "${PORT}:${PORT}" # phần này ta định nghĩa ở file .env nhé
restart: unless-stopped
depends_on:
- redis
- db
networks:
- db-network
- redis-network
healthcheck:
test: wget --quiet --tries=1 --spider http://localhost:${PORT} || exit 1z
interval: 30s
timeout: 10s
retries: 5
db:
image: mongo:4.4
volumes:
- .docker/data/db:/data/db
restart: unless-stopped
networks:
- db-network
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongo db:27017/speech-api --quiet
interval: 30s
timeout: 10s
retries: 5
redis:
image: redis:5-alpine
volumes:
- .docker/data/redis:/data
restart: unless-stopped
networks:
- redis-network
healthcheck:
test: ["CMD", "redis-cli","ping"]
interval: 30s
timeout: 10s
retries: 5
#Docker Networks
networks:
db-network:
driver: bridge
redis-network:
driver: bridge
```
* Các thành phần:
* service app:
* HEALTHCHECK:
* test:
* wget --quiet --tries=1 --spider http://localhost: ${PORT} || exit 1z
* Dùng localhost vì việc check là ở trong container
* Tạo 1 request đến app, nếu status code là 200 thì healthy, khác 200 thì unhealthy
* interval: 30s
* 30s sau khi container được khởi tạo thì mới healthcheck, cứ 30s là begin healthcheck 1 lần
* timeout: 10s
* Nếu thời gian chờ đợi ng ấy hồi âm quá lâu (10s), coi như ta đã thất bại, như khi ta tạo request tới 1 URL nào đó cần nhiều thời gian phản hồi => Toang lun
* retries: 5
* Nếu test là unhealthy thì test thêm 5 lần nữa để đảm bảo ng ấy đã nhận nhưng ko chọn ta.
* db và redis cũng test như thế, khác command để test
* Check sau khi healthcheck
> docker-compose ps
> docker container ls

## Bảo mật ứng dụng Docker NodeJS, Mongo, Redis
### Security
#### Thêm Docker network
```yaml
version: "3.4"
services:
app:
image: learning-docker:node
volumes:
- ./public/images:/app/public/images
environment:
- DB_HOST=${DB_HOST}
- DB_NAME=${DB_NAME}
- REDIS_HOST=${REDIS_HOST}
- REDIS_PORT=${REDIS_PORT}
- PORT=${PORT}
ports:
- "${PORT}:${PORT}"
restart: unless-stopped
depends_on:
- redis
- db
networks: // -----------Note here
- db-network
- cache-network
db:
image: mongo:4.4
volumes:
- .docker/data/db:/data/db
restart: unless-stopped
networks: // ---------------Note here
- db-network
redis:
image: redis:5-alpine
volumes:
- .docker/data/redis:/data
restart: unless-stopped
networks: // --------------Note here
- cache-network
networks: // --------------Note here
cache-network:
driver: bridge
db-network:
driver: bridge
```
### Authentication cho Mongo và Redis
#### Mongo
* Best practice là tạo db với non-root user
* Tránh 1 user có thể đọc toàn bộ CSDL
##### setup authentication cho Mongo DB:
Tạo file db-entrypoint.sh ở folder .docker :
```dockerfile
echo 'Creating application user and db'
mongo ${DB_NAME} \
--host localhost \
--port ${DB_PORT} \
-u ${MONGO_INITDB_ROOT_USERNAME} \
-p ${MONGO_INITDB_ROOT_PASSWORD} \
--authenticationDatabase admin \
--eval "db.createUser({user: '${DB_USER}', pwd: '${DB_PASSWORD}', roles:[{role:'dbOwner', db: '${DB_NAME}'}]});"
```
* File này dùng để: tạo user với username, password; sau đó tạo db và gán nó với user đó
* File này dc đọc và apply khi db được chạy
* Thêm biến env vào .env:
```yaml
PORT=3000
DB_HOST=db
DB_PORT=27017
DB_NAME=my_db
DB_ROOT_USER=rootuser
DB_ROOT_PASS=rootuserpass
DB_USER=myuser
DB_PASSWORD=myuserpass
REDIS_HOST=redis
REDIS_PORT=6379
```
* Ở trên chúng ta có thông tin của rootuser và myuser, rootuser sẽ được dùng trong db-entrypoint.sh để tạo myuser tại thời điểm ban đầu.
3. Sửa lại file docker compose ở app và db
```yaml
...
app:
image: learning-docker:node
volumes:
- ./public/images:/app/public/images
environment:
- DB_HOST=${DB_HOST}
- DB_PORT=${DB_PORT}
- DB_NAME=${DB_NAME}
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_HOST=${REDIS_HOST}
- REDIS_PORT=${REDIS_PORT}
- PORT=${PORT}
ports:
- "${PORT}:${PORT}" # phần này ta định nghĩa ở file .env nhé
restart: unless-stopped
depends_on:
- redis
- db
networks:
- db-network
- cache-network
db:
image: mongo:4.4
volumes:
- .docker/data/db:/data/db
- .docker/db-entrypoint.sh:/docker-entrypoint-initdb.d/db-entrypoint.sh
restart: unless-stopped
networks:
- db-network
environment:
- MONGO_INITDB_ROOT_USERNAME=${DB_ROOT_USER}
- MONGO_INITDB_ROOT_PASSWORD=${DB_ROOT_PASS}
- DB_PORT=${DB_PORT}
- DB_NAME=${DB_NAME}
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
...
```
* Các thành phần:
* app sửa lại env
* db thêm mount .docker/db-entrypoint.sh vào container ở /docker-entrypoint-initdb.d/db-entrypoint.sh; nó sẽ chạy tất cả file .sh ở folder đấy.
* db thêm các biến env; chú ý user root phải dc đặt là "MONGO_INITDB_ROOT_..." ; các biến phải khớp với file .sh
4. Sửa lại app.js connect với db
```javascript
...
const dbHost = process.env.DB_HOST || 'localhost'
const dbPort = process.env.DB_PORT || 27017
const dbName = process.env.DB_NAME || 'my_db_name'
const dbUser = process.env.DB_USER
const dbUserPassword = process.env.DB_PASSWORD
const mongoUrl = `mongodb://${dbUser}:${dbUserPassword}@${dbHost}:${dbPort}/${dbName}`
...
```
5. Build lại image
> docker build -t learning-docker:node .
> docker compose down
* Xóa db cũ
```dockerfile
# tìm tên chính xác của volume MongoDB
docker volume ls
--->>>>
......
local secure-docker-node-mongo-redis_mongodata
# xóa volume, kiểm tra đúng tên project trước khi xóa nhé ;)
docker volume rm secure-docker-node-mongo-redis_mongodata
```
#### Cách xem database trong docker sau khi authentication
```dockerfile
docker-compose exec db sh
mongo
use my_db # tên database của chúng ta
db.auth("myuser", "myuserpass") # chạy command này thấy in ra "1" là OK nhé
# Từ đây thì chúng ta có thể chạy mọi command khác với MongoDB như bình thường. Ví dụ:
show collections
->>products
->>users
```
* Nếu mongo bị deprecated (ko dc dùng nữa) thì dùng mongosh
#### Redis
1. Sửa lại file .env
```yaml
...
REDIS_HOST=redis
REDIS_PORT=6379
REDIS_PASSWORD=redispass
```
* Thêm REDIS_PASSWORD=redispass vào
2. Sửa lại docker compose file ở redis và app
```yaml
...
app:
...
environment:
...
- REDIS_PASSWORD=${REDIS_PASSWORD} # thêm vào biến này
redis:
image: redis:5-alpine
volumes:
- .docker/data/redis:/data
command: redis-server --requirepass ${REDIS_PASSWORD} # thêm vào duy nhất dòng này
restart: unless-stopped
networks:
- cache-network
```
* command dùng để chạy 1 command ngay tại thời điểm mà service được khởi động lên.
3. Sửa lại file nodejs
```javascript
...
const client = redis.createClient({
host: process.env.REDIS_HOST || '127.0.0.1', // this must match the container name of redis image
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD // -->>>>thêm vào duy nhất chỗ này
})
...
```
> docker build -t learning-docker:node .
> docker compose down
> docker compose up
### Chạy app với non-root user
1. Viết lại dockerfile
```dockerfile
FROM node:12.18-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm install -g pm2
# Create a group and user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
RUN chown -R appuser:appgroup /app
# Tell docker that all future commands should run as the appuser user
USER appuser
CMD ["pm2-runtime", "ecosystem.config.js", "--env", "production"]
```
* Các thành phần:
* RUN addgroup -S appgroup && adduser -S appuser -G appgroup
* Ta thêm group là appgroup và appuser:appgroup trong appgroup
* RUN chown -R appuser:appgroup /app
* Cấp quyền thực thi của appuser:appgroup trên folder app (option -R - recursive)
* USER appuser
* Cuối cùng là ta đổi user hiện tại thành appuser. Kể từ đây tất cả các command sẽ được chạy dưới danh nghĩa appuser.
* Test
> docker-compose exec app sh
> whoami
->> appuser
ls -l

* thử cài cái gì đó xem appuser có quyền không nhé, điều ta mong muốn là appuser không có quyền vì không phải root

### Healthcheck
```yaml
version: "3.4"
services:
app:
image: learning-docker:node
volumes:
- ./public/images:/app/public/images
environment: # phần này ta định nghĩa ở file .env nhé
- DB_HOST=${DB_HOST}
- DB_PORT=${DB_PORT}
- DB_NAME=${DB_NAME}
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
- REDIS_HOST=${REDIS_HOST}
- REDIS_PORT=${REDIS_PORT}
- REDIS_PASSWORD=${REDIS_PASSWORD}
- PORT=${PORT}
ports:
- "${PORT}:${PORT}" # phần này ta định nghĩa ở file .env nhé
restart: unless-stopped
depends_on:
- redis
- db
networks:
- db-network
- cache-network
healthcheck:
test: wget --quiet --tries=1 --spider http://localhost:${PORT} || exit 1z
interval: 30s
timeout: 10s
retries: 5
db:
image: mongo:4.4
volumes:
- .docker/data/db:/data/db
- .docker/db-entrypoint.sh:/docker-entrypoint-initdb.d/db-entrypoint.sh
restart: unless-stopped
networks:
- db-network
environment:
- MONGO_INITDB_ROOT_USERNAME=${DB_ROOT_USER}
- MONGO_INITDB_ROOT_PASSWORD=${DB_ROOT_PASS}
- DB_PORT=${DB_PORT}
- DB_NAME=${DB_NAME}
- DB_USER=${DB_USER}
- DB_PASSWORD=${DB_PASSWORD}
healthcheck:
test: echo 'db.runCommand("ping").ok' | mongo mongodb://${DB_USER}:${DB_PASSWORD}@localhost:${DB_PORT}/?authSource=${DB_NAME} --quiet
interval: 30s
timeout: 10s
retries: 5
redis:
image: redis:5-alpine
volumes:
- .docker/data/redis:/data
command: redis-server --requirepass ${REDIS_PASSWORD}
restart: unless-stopped
networks:
- cache-network
healthcheck:
test: ["CMD", "redis-cli","ping"]
interval: 30s
timeout: 10s
retries: 5
networks:
cache-network:
driver: bridge
db-network:
driver: bridge
```
#### Additional
* service app có quá nhiều biến môi trường env
```yaml
...
app:
image: learning-docker:node
volumes:
- ./public/images:/app/public/images
env_file: .env # ------------>>>>>> Note dòng này
ports:
- "${PORT}:${PORT}"
restart: unless-stopped
depends_on:
- redis
- db
networks:
- db-network
- cache-network
healthcheck:
test: wget --quiet --tries=1 --spider http://localhost:${PORT} || exit 1z
interval: 30s
timeout: 10s
retries: 5
...
```
* thay environment bằng env_file: .env
* load toàn bộ file .env làm biến môi trường cho tôi nhé
## Tối ưu docker image
## chạy ứng dụng container với Non-Root User
* image Docker được build sẵn mà ta thường dùng thì đều chạy với user root
docker pull image_name:tag
docker images
docker ps
docker ps -a
docker start container_id
docker stop container_id
docker restart container_id
docker rm container_id
docker rmi image_id
docker run -d -p host_port:container_port image_id
docker run -it image_id
docker run -dit image_id
docker exec -it container_id bin/bash
docker build -t image_name:latest .
docker run -p port image_name (port)
docker run image_name -p (param)
rm -rf devops-for-beginner: xóa folder
docker compose -f docker-compose.yaml up -d
docker compose down
docker inspect
### ECR
* Use case: Project ko cho dùng docker hub để lưu image mà bắt phải trên cloud
* Lưu ở mỗi registry
### ECS
* Thay vì dùng ec2 cài docker lên thì ta quản lý
* tính năng:
* easy manage
* docker trên local chạy sao thì ec2 chạy như thế
* Kết hợp với autoscaling, scale in and out
* Service như elb, ecr, iam, cloudwatch,...
* cung cấp 2 kiến trúc là ec2 hay fargate (customize nhiều thì chọn ec2)
* thành phần:
* cluster: cung cấp resource như ec2, fargate chạy app
* task: cung cấp tài nguyên CPU, RAM trong mode fargate. task có thể chứa 1 hay nhiều container
* service: nhóm các task
* container: giống docker container
* ecs connect service: connect 2 services với nhau
* task definition: chỉ dẫn task tạo ntn, giống launch template.
* Có thể có 1 số task chạy riêng lẻ mà ko trong service vì khi chạy xong nó kill task lun.
* Task def giúp cho cluster biết nên pull image nào về để chạy các task.
*
### Docker 101