11B-43 NGUYỄN DUY TIẾN
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    # 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ơ") * ![](https://hackmd.io/_uploads/BJlB8Wpep.png) * 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 ![](https://hackmd.io/_uploads/BJgrPZpg6.png) * 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 ![](https://hackmd.io/_uploads/HkpCPZTeT.png) ### 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 ![](https://hackmd.io/_uploads/By4tOWaxT.png) * **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 * ![](https://hackmd.io/_uploads/S1G8MMYWa.png) * 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 ![](https://hackmd.io/_uploads/BkGx6Gt-6.png) * Và nó bị lỗi: ![](https://hackmd.io/_uploads/By7d6zK-6.png) * 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 ![](https://hackmd.io/_uploads/SkNY-cYWa.png) #### 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 ![](https://hackmd.io/_uploads/H1GJOoKWa.png) * Đầ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 ![](https://hackmd.io/_uploads/SkUoqjYZT.png) * 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 ![](https://hackmd.io/_uploads/r1uKost-6.png) > curl redis:6379 ![](https://hackmd.io/_uploads/SyHcsjt-T.png) > docker-compose exec redis sh apk add curl curl db:27017 ![](https://hackmd.io/_uploads/Bk0posFZT.png) ### 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 ![](https://hackmd.io/_uploads/BJF44z5Za.png) ## 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 ![](https://hackmd.io/_uploads/HkhoFXq-6.png) * 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 ![](https://hackmd.io/_uploads/r1UCFmcZa.png) ### 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

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully