# Mở đầu Ngày nay, việc phát triển web có rất nhiều lựa chọn, không còn gói gọn trong những stack lâu đời như LAMP, Ruby on Rails ... nữa. Đặc biệt nổi lên gần đây là MEAN Stack (MongoDB, ExpressJS, AngularJS, NodeJS), sử dụng hoàn toàn 1 loại ngôn ngữ là Javascript để phát triển website (à đương nhiên vẫn có HTML, CSS (yaoming) ). Việc cài đặt môi trường để phát triển sử dụng MEAN Stack cũng là một vấn đề nếu bạn không quen sử dụng Linux, Mac, hay đang dùng Windows (tin tôi đi, bạn không muốn cài đặt cái gì để dev trên Win đâu, hiện thời nó vẫn là 1 cực hình :D). Do đó trong bài viết này, tôi sẽ giới thiệu với các bạn một cách sử dụng Docker và Docker-Compose để thiết lập môi trường lập trình với MEAN Stack mà có thể chạy tốt trên Windows, Mac hay Linux, đúng với tinh thần lười của anh em ta, install one and develop anywhere # Cài đặt Docker Nếu như Windows, Mac và Linux là các môi trường khác nhau dùng để khởi chạy máy tính, thì khi phát triển phần mềm, chúng ta mong muốn có một môi trường thống nhất và gần với môi trường thực tế hệ thống sẽ sử dụng nhất để tránh những khó khăn có thể gặp phải khi cài đặt môi trường khác nhau, hoặc "em không hiểu sao máy em chạy ngon mà server thì lỗi". Và Docker là một ứng dụng được sinh ra để làm điều này. Với những khả năng như ABC, xyz ... (ABC, xyz là gì mời các bạn tra Google), Docker sẽ cho chúng ta một môi trường tách biệt hoàn toàn với môi trường của hệ điều hành trong máy của chúng ta, và đảm bảo cho dù bạn có sử dụng trên hệ điều hành nào đi nữa thì môi trường đó vẫn *trước sau như một*. Nguyên lý khi sử dụng Docker là mỗi lần chạy Docker, bạn sẽ khởi tạo một **container** từ một **image** đã được tạo ra, container đó sẽ chạy **một câu lệnh** để làm một tác vụ gì đó. Dựa trên đặc điểm này, chúng ta sẽ tạo hai image, một image cho việc chạy database MongoDB, một image cho việc chạy Server NodeJS sử dụng ExpressJS. Lý do tại sao lại sử dụng 2 image như sau : 1. 2 image cho 2 việc khác nhau, 1 là lưu trữ dữ liệu (DB), 1 là server web, đúng tinh thần server nào làm việc đó, không ôm đồm. 2. Việc cấu hình cho 2 image làm 2 việc này dễ và sẵn có, hầu hết chỉ có kéo về và chạy thôi (yaoming). Đương nhiên vẫn có cách để tạo một image chứa cả NodeJS và MongoDB, các bạn có thể thử với thứ tự sau : 1. Tạo một Image chạy Centos 2. Thêm các câu lệnh cài đặt NodeJS 3. Thêm các câu lệnh cài đặt MongoDB 4. Thiết lập câu lệnh khi khởi chạy container thì đồng thời khởi chạy cả NodeJS và MongoDB Và sau đây tôi xin hướng dẫn sử dụng cách đầu tiên, mỗi image một chức năng khác nhau bằng cách sử dụng **Docker-compose** cho nó đơn giản. Tuy nhiên trước hết chúng ta cần cài Docker, tạo file Dockerfile để cài NodeJS, các bạn có thể dùng các image sẵn có trên mạng, tuy nhiên cá nhân tôi dùng file setup riêng từ CentOS 6, vì server tôi sẽ deploy là dùng CentOS 6, các image của Node từ Docker dùng image khác, thừa này thiếu nọ, tôi không thích vì sẽ khác với môi trường production :D Dưới đây là file Dockerfile của tôi: ``` FROM centos:6 RUN yum -y update RUN yum -y groupinstall "Development Tools" RUN yum -y install cronie # gpg keys listed at https://github.com/nodejs/node RUN set -ex \ && for key in \ 9554F04D7259F04124DE6B476D5A82AC7E37093B \ 94AE36675C464D64BAFA68DD7434390BDBE9B9C5 \ 0034A06D9D9B0064CE8ADF6BF1747F4AD2306D93 \ FD3A5288F042B6850C66B31F09FE44734EB7990E \ 71DCFD284A79C3B38668286BC97EC7A07EDE3FC1 \ DD8F2338BAE7501E3DD5AC78C273792F7D83545D \ B9AE9905FFD7803F25714661B63B535A4C206CA9 \ C4F0DFFF4E8C1A8236409D08E73BC641CC11F4C8 \ ; do \ gpg --keyserver ha.pool.sks-keyservers.net --recv-keys "$key"; \ done ENV NPM_CONFIG_LOGLEVEL info ENV NODE_VERSION 4.4.5 RUN curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-x64.tar.xz" \ && curl -SLO "https://nodejs.org/dist/v$NODE_VERSION/SHASUMS256.txt.asc" \ && gpg --batch --decrypt --output SHASUMS256.txt SHASUMS256.txt.asc \ && grep " node-v$NODE_VERSION-linux-x64.tar.xz\$" SHASUMS256.txt | sha256sum -c - \ && tar -xJf "node-v$NODE_VERSION-linux-x64.tar.xz" -C /usr/local --strip-components=1 \ && rm "node-v$NODE_VERSION-linux-x64.tar.xz" SHASUMS256.txt.asc SHASUMS256.txt CMD [ "node" ] RUN mkdir -p /usr/src/app COPY . /usr/src/app WORKDIR /usr/src/app RUN chmod +x startup.sh RUN npm install -g nodemon ``` Giải thích: Các câu lệnh trên sẽ lần lượt lấy image CentOS 6 làm gốc, cài các lib dev cần thiết, download và cài node phiên bản 4.4.5, sau đó copy thư mục hiện hành vào thư mục `/usr/src/app` và set thư mục mặc định chạy là `usr/src/app`. Sau khi copy set quyền execute cho file `startup.sh` và cài đặt `nodemon` Việc copy thư mục hiện hành và set thư mục mặc định sẽ giúp các bạn việc sau: code của các bạn ở trên thư mục chứa file Dockerfile sẽ được copy vào container trên Docker và có thể dùng để chạy Node server. 2 lệnh set chmod cho `startup.sh` và cài đặt `nodemon` là optional, cái này mình sẽ giải thích sau, đại khái là giúp bạn dev sướng hơn :D Vậy là đã xong file Dockerfile. Tiếp đến sẽ là Docker-compose # Cài đặt và cấu hình Docker-compose Sau khi có file Dockerfile, mình sẽ sử dụng file image này cho web server (NodeJS, Express), còn Database mình sẽ sử dụng image MongoDB có sẵn của Docker. Tuy nhiên việc cài đặt, rồi link giữa 2 container khá là lằng nhằng, mất thời gian, nên mình sẽ sử dụng tool có tên là **Docker-compose** để làm hộ mình. Giá trị của **Docker-compose** sẽ là giúp các bạn khởi chạy nhiều container, liên kết chúng vào với nhau thông qua 1 file setting, giúp chúng ta không cần viết script mỗi lần khởi chạy nữa, chỉ cần một câu lệnh `docker-compose up` là môi trường của bạn sẽ sẵn sàng để làm việc. Không nói nhiều nữa, dưới đây là nội dung file `docker-compose.yml` cần thiết cho việc chạy `docker-compose`: ``` version: '2' services: mongo: image: mongo ports: - "32769:27017" web: build: . working_dir: /usr/src/app command: /bin/bash startup.sh volumes: - .:/usr/src/app ports: - "32768:3000" depends_on: - mongo ``` Chi tiết ý nghĩa của từng dòng lệnh các bạn có thể tìm hiểu trên google, ở đây ý nghĩa của chúng đại khái là: 1. Tạo 2 service có tên là `mongo` và `web` 2. Service mongo sử dụng image có tên là mongo 3. Port của mongo là 27017 ở trong container, ở ngoài hệ điều hành thật là 32769 4. Service web sử dụng image build từ thư mục hiện hành (là thư mục chứa file Dockerfile ở trên) 5. Service web có thư mục làm việc là `/usr/src/app`, có volumes là thư mục hiện tại đến thư mục `/usr/src/app`, nhằm mục đích khi thay đổi nội dung trên thư mục hiện tại ( code ), thì dữ liệu sẽ lập tức được cập nhật trên thư mục `/usr/src/app` 5. Service web có command khởi chạy là `/bin/bash startup.sh` 6. Service web phụ thuộc vào service mongo, nếu service mongo không chạy thì service web sẽ không hoạt động. Vậy là xong, bạn đã tạo ra 2 service có thể dùng để chạy server NodeJS và server database (mongo), còn một bước cuối cùng sẽ là file `package.json` cùng file `startup.sh`. File `package.json` (có thể tạo bằng `npm init`, nhưng trong trường hợp bạn không thích cài npm lên OS thật, thì copy ở đây là xong :D): ```javascript { "name": "appname", "version": "1.0.0", "description": "app description", "main": "app.js", "scripts": { "start": "node app.js" }, "authors": [ "Ta Duy Anh <anhtd37@gmail.com>" ], "dependencies": { "aws-sdk": "^2.3.12", "bluebird": "^2.9.32", "body-parser": "^1.13.1", "change-case": "^2.3.0", "cheerio": "^0.19.0", "cors": "^2.7.1", "crypto": "0.0.3", "debug": "^2.2.0", "express": "^4.13.0", "formidable": "^1.0.17", "gm": "^1.23.0", "kue": "^0.11.1", "kue-scheduler": "^0.6.0", "lodash": "^4.13.1", "moment": "^2.10.3", "mongo-express": "^0.30.59", "mongoose": "^4.4.17", "multer": "^1.1.0", "mysql": "^2.0.0", "node-cron": "^1.1.1", "path-to-regexp": "^1.5.0", "query-string": "^4.2.2", "redis": "^0.12.1", "request": "^2.58.0", "request-promise": "^4.1.1", "sequelize": "^3.3.2", "sleep": "^3.0.1" } } ``` Ở đây file của mình hơi nhiều dependencies, nhưng chủ yếu bạn chỉ cần express + mongooes lúc đầu là đủ :D Tiếp theo là file `startup.sh`: ```bash if [ ! -d /usr/src/app/node_modules ]; then echo "Install dependencies..." cd /usr/src/app && npm install --no-bin-links fi cd /usr/src/app && nodemon -L app.js ``` File này sẽ là file khởi chạy của service web mỗi khi `docker-compose up`. Mục đích của file này là sẽ chạy lệnh `npm install` nếu như không tìm thấy thư mục *node_modules* trong thư mục hiện tại, với các đặc điểm sau: 1. Cài đặt các node_modules cho việc phát triển một cách tự động 2. Tránh việc phải upload node_modules lên repo 3. Vì cài đặt bên trong container nên nếu muốn update node_modules, cần vào container -> suy nghĩ kỹ trước khi update =)) Sau khi xong xuôi các file trên, các bạn sẽ có một môi trường chạy ExpressJS cùng MongoDB để phát triển, nếu muốn dùng MySQL, các bạn chỉ cần cần đổi service database sang mysql (hoặc thêm service), sau đó nhớ add depends để service db chạy trước service web. Một lưu ý nữa là mình dùng `nodemon` để khởi động web server với câu lệnh : `nodemon -L app.js`. Trong trường hợp webservice của các bạn sử dụng file khởi chạy là file khác, mời đổi lại tên file. Đồng thời việc sử dụng `nodemon` sẽ giúp code của các bạn ngay khi có thay đổi sẽ được cập nhật lên web service bằng cách... khởi động lại web service :D Vì thời gian khởi động webservice của Express rất là không đáng kể, nên bạn có thể bật Sublime, bấm Ctrl + S liên tục để xem output thông báo server khởi động lại của ExpressJS :D Ngoài ra có một lưu ý nhỏ khi dùng service mongo db như trên thì connection link sang MongoDB sẽ là : `mongo/<database-name>` với `mongo` là tên service. Ở đây mình dùng cổng mặc định là 27017 nên không cần phải config cổng, nếu dùng cổng khác các bạn tự thêm thành `mongo:<tên_cổng>` là ok. Trong trường hợp bạn muốn update file package.json thì sao? Chúng ta sẽ có những cách sau: 1. Edit file package.json, thêm thư viện cần thêm, sau đó vào `/bin/bash` của container và chạy `npm install`. Cách vào `/bin/bash` của container : `docker exec -it /bin/bash <container_name>` 2. Update file package.json, xóa thư mục node_modules và chạy lại lệnh `docker-compose up` 3. Cài npm lên hệ điều hành hiện tại và `npm install --save <package_name>` bình thường Trên đây là hướng dẫn cài đặt môi trường phát triển MEAN Stack sử dụng Docker đang được mình sử dụng trong công việc, hy vọng rằng sẽ giúp ích được cho các bạn muốn làm quen với NodeJS :D