ลอง Setup โปรเจค Docker + Node.js ง่ายๆ สำหรับ Dev และ Production

Kan Aruncharathorn
InsightEra
Published in
3 min readDec 11, 2019
Photo by Todd Cravens on Unsplash

ประเด็นหลักที่ผมต้องการหาวิธี setup แบบนี้ ก็คือ ความกังวลเรื่อง …

“ถ้าเครื่องผมไม่ใช่ Linux แล้วไม่ได้ dev ใน Docker เนี่ย จะรู้ได้ยังไงว่า Code พวกนี้มันจะไป run ใน Linux ใน Docker ได้จริงๆ”

ซึ่งถ้ามันดันมีพวก dependencies ที่ใช้ได้ใน Mac หรือ Windows แต่ใช้ไม่ได้ใน Linux ขึ้นมา ก็น่าจะเป็นปัญหาได้นะ งั้นก็ dev กับ Docker image ที่มันจะได้ใช้งาน ใน Production ตั้งแต่แรกไปเลยละกัน

หลังจากที่ลองอ่านหลายๆบทความ เกี่ยวกับการ setup Node.js ใน Docker โดยที่สามารถ dev ผ่าน Docker container ในเครื่องเราได้เลย ก็พบว่าน่าสนใจ จึงทดลองใช้ดู แต่ก็ยังมีบางอย่างที่ต้องปรับเปลี่ยนนิดหน่อย เพื่อให้ใช้งานได้แบบที่ผมต้องการ จึงมาสรุปเป็นบทความนี้ครับ

TLDR; ใช้ Dockerfile เพื่อ Production และใช้ docker-compose ในการแก้ environment variable กับ command ให้ใช้สำหรับตอน Development,ใช้ nodemon ในการ Reload เมื่อมีการแก้ไขไฟล์ และมีเพิ่ม Config นิดนึงให้สามารถ install dependencies ทีหลังโดยไม่ต้องปิด docker-compose กับ Config ที่ทำให้ใช้ debugger ใน VScode ผ่าน docker ได้

เริ่มลงมือกันเลย

ก่อนเริ่ม ก็ต้องมี npm กับ Docker ในเครื่องก่อนนะ

อันดับแรกเริ่มสร้างโฟลเดอร์โปรเจคใหม่ แล้ว npm init ลงพวก dependencies ได้เลย โดยตัวอย่างของผมรอบนี้หลักๆจะใช้ express, ioredis และก็อย่าลืม nodemon ที่เราจะเอาไว้ reload server ตอน dev ด้วยนะ

โดยเราจะเพิ่ม script เข้าไปใน package.json ในส่วน start กับ dev แบบนี้ครับ

ไฟล์ package.json

จากนั้น สร้างไฟล์ Dockerfile ซึ่งเป็นแบบที่เอาไป build แล้วได้ image ปกติ นำไป deploy ใช้ใน Production ได้เลย ซึ่งโปรเจคทดลองผมมีแค่นี้ครับ

ไฟล์ Dockerfile

แล้วก็อย่าลืมเพิ่มไฟล์ .dockerignore ไว้ด้วย ป้องกัน การส่งไฟล์ หรือ ​โฟลเดอร์เหล่านี้ เข้าไปตอน build image ครับ

ไฟล์ .dockerignore

ทีนี้เริ่มต้น สร้างไฟล์ docker-compose.yml ซึ่งปัญหาหลักช่วงแรกที่เจอก็คือ การ bind volume ที่ชอบมีปัญหากับ node_modules ในเครื่องผมเอง ก็เลยเสิร์ชจน พบคำตอบว่าต้องสร้าง volume สำหรับ node_modules แยกไปเลย ในกรณีนี้ผมตั้ง volume อันนึงไว้ ชื่อว่า dependencies

docker-compose เวอร์ชันแรก

แค่นี้ก็เกือบจะเพียงพอต่อการใช้งานแล้ว ด้วยการรันคำสั่ง

 docker-compose up --build

แต่ว่า.. ถ้าเราจะเพิ่ม dependency ด้วยการ npm install เนี่ย ก็ต้องปิด docker-compose แล้วเปิดใหม่ เพื่อให้มัน build image ขึ้นมาใหม่ด้วยคงจะเสียเวลานาน

หลังจากที่พยายามลองหา ว่าคนอื่นๆ ใน Internet เค้าแก้ปัญหากันยังไง ก็พบบทความอันนึง ที่ใช้วิธีสร้าง docker-compose V2 มาอีกไฟล์นึง แล้วใช้ extends base image ที่ bind volume เดียวกัน เพื่อให้มันเรียก service ที่จะ npm install เพียงแต่ผมทำแบบนั้นไม่ได้ เพราะ Config เริ่มต้นมาไม่เหมือนกัน แถมยังไม่อยากสร้างอีกไฟล์ แต่ก็พอได้เป็นไอเดีย จึงทดลองทำแบบนี้ดูครับ .

docker-compose เวอร์ชันปรับปรุง

โดยการใส่ service ใหม่อันนึง ชื่อว่า install ที่จะใช้ image และ volume dependencies อันเดียวกับ app แล้วเรียกใช้ command npm install แทน
โดยมี ทริคนิดนึง คือแทนที่เราจะ `npm install` เฉยๆ เราต้องเพิ่ม `touch index.js` เข้าไปด้วย เพื่อให้ตัว nodemon ข้างใน docker มันเข้าใจว่ามีการแก้ไข
จึงสามารถ reload server ให้เราหลัง install เสร็จได้เลยครับ
แต่ทว่า command มันดันใส่ได้คำสั่งเดียว เราจึงต้องใช้ bash มาช่วย รวมหลายๆ คำคำสั่งนั่นเองครับ

command: bash -c "npm install && touch index.js"

ปล. ถ้าใช้ alpine หรือ Linux distro อื่นที่ไม่มี bash ก็ใช้ sh แทนได้นะครับ

เพียงเท่านี้เราก็สามารถ ลง dependency ข้างนอก ด้วยคำสั่ง npm install แล้วสั่ง command เพิ่มดังนี้ได้เลย

npm install foo...docker-compose run --rm install

ซึ่งเป็นการสั่งให้ service ที่ชื่อว่า install ทำงานขึ้นมา แล้วจะลบ container ออกหลังจบการทำงาน

หรือจะสร้าง Makefile แล้วรัน make install แบบนี้ก็เท่ดี มาจาก บทความนี้

npm install foo...make install

หรือ ถ้าใครขี้เกียจไปอีกขั้น ไม่อยากจะ run คำสั่ง install เอง ก็อาจจะลองหา plugin ที่มาเช็ค event install แล้วให้มันทำงาน automate เอง ก็คิดว่าเป็นได้ครับ แต่เท่าที่ผมลองทำเองกับ npm script พวก postinstall อะไรแบบนี้มันไม่ trigger แฮะ อดเลย…

แถม

ทีนี้บางคนอาจจะชอบใช้ debugger ใน VScode งั้นก็รวมวิธี Config ให้ Debug ด้วย VScode ได้เข้าไปเลยละกัน

ขั้นตอนแรก แก้ package.json ตรง script ของ dev เป็น

#package.json..."dev": "nodemon --inspect=0.0.0.0 ./index.js",...

แล้วก็เพิ่ม port สำหรับ inspector ใน docker-compose.yml

#docker-compose.yml...ports:
- 3000:3000
- 9229:9229
...

หลังจากนั้นไปที่ VScode เปิดแท็บ Debugger แล้วกด “Add Configuration” จะได้ไฟล์ launch.json มา ใส่ Config ไปแบบนี้ได้เลย

ไฟล์ launch.json อยู่ในโฟลเดอร์ .vscode

หลังจากนั้นก็ ลองรัน docker-compose --build ใหม่ แล้วก็ลองกด Debug ด้วย Configuration ที่ชื่อ “Docker: Attach to Node” จากใน VScode มันจะ Attach กับ inspector ให้ได้เลย ทีนี้ก็ใช้ breakpoint กันได้แล้วครับ

สรุป

เท่าที่ผมได้ลองใช้เอง ก็ถือว่าใช้ได้ระดับนึง สำหรับโปรเจคง่ายๆ แต่ก็ยังไม่ได้นำไปทดลองกับโปรเจคที่ใหญ่ หรือ ซับซ้อนกว่านี้ ผมคิดว่ายังมีอะไรให้ปรับปรุงอีกเยอะ และก็ยังมีจุดที่อยากปรับเพิ่มเติม ก็คือ เรื่องที่ยังต้องเรียกคำสั่งด้วยมืออีกครั้ง เวลาลง Dependency ซึ่งถ้าให้มันทำงานอัตโนมัติหลัง npm install ข้างนอกได้เลย น่าจะสะดวกกว่ามากๆ ในตอน Dev .. เอาไว้ถ้าเจอวิธีแก้จะมาอัพเดทนะครับ ~

UPDATE V.2 แก้ปัญหาเรื่อง postinstall (May 2020)

จากที่ไปทำการหาต้นตอปัญหา ก็พบว่าเป็น Bug ของ npm ที่ไม่ยอมเรียก postinstall อย่างถูกต้องครับ ก็เลยแก้ด้วยวิธีต่อไปนี้ ภายในสองขั้นตอนครับ

ขั้นตอนแรก เราต้องเปลี่ยนจาก npm เป็น yarn ภายใน docker-compose.yml ใน service install ครับ จาก

command: bash -c "npm install && touch index.js"

เป็น

command: sh -c "yarn install && touch index.js"

แล้วตามด้วยการแก้ package.json ให้ทำ postinstall และ postuninstall โดยมีเงื่อนไขง่ายๆว่า ต้องไม่อยู่ใน docker environment เท่านั้นจึงจะทำงาน โดยเพิ่มโค้ดเหล่านี้ลงไปครับ

package.json
...
"scripts": {
"dev": "nodemon --inspect=0.0.0.0 ./index.js",
"start": "node ./index.js",
"test": "mocha",
"postinstall": "[ -f /.dockerenv ] && echo \"Skip ...\" || make install",
"postuninstall": "[ -f /.dockerenv ] && echo \"Skip ...\" || make install"
},
...

เพียงเท่านี้เราก็สามารถใช้งาน ได้โดยไม่ต้องสั่ง make install ซ้ำแล้ว เย้ :P
ครั้งต่อไปที่ต้องการ install, uninstall ก็ใช้ Yarn สั่งจากข้างนอกแทนได้เลยครับ
เช่น yarn add ... หรือ yarn remove ... เป็นต้น

อันนี้เป็นลิ้งก์โปรเจคใน Github ครับ

บทความอันนี้เป็นการเขียนครั้งแรกของผม หากใครมีข้อเสนอแนะ หรือ มีสิ่งที่น่าจะแก้ไข รบกวนบอกหรือคอมเม้นมาได้เลยครับ :)

--

--